Fix Mechyrdia Sans kerning
authorLanius Trolling <lanius@laniustrolling.dev>
Sun, 3 Mar 2024 13:29:29 +0000 (08:29 -0500)
committerLanius Trolling <lanius@laniustrolling.dev>
Sun, 3 Mar 2024 13:29:29 +0000 (08:29 -0500)
73 files changed:
.idea/gradle.xml
build.gradle.kts
fontparser/LICENSE [new file with mode: 0644]
fontparser/build.gradle.kts [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiClass.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiConstants.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/AdvancedTypographicTableFormatException.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassMapping.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassTable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphContextTester.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageMapping.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageTable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinition.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionSubtable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionTable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphMappingTable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioning.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningState.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningSubtable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningTable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphProcessingState.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Positionable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Substitutable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ArabicScriptProcessor.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DefaultScriptProcessor.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DevanagariScriptProcessor.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GujaratiScriptProcessor.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GurmukhiScriptProcessor.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/IndicScriptProcessor.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ScriptProcessor.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/TamilScriptProcessor.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharAssociation.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharScript.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CMapSegment.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CodePointMapping.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Font.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontMetrics.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontTriplet.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontType.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontUtil.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitution.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionState.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionSubtable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionTable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubtable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Glyphs.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFAdvancedTypographicTableReader.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFLanguage.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFScript.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/SingleByteEncoding.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Typeface.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/io/ByteArrayOutputStream.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/io/Charsets.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/io/ClosedInputStream.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/io/IOUtils.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/io/LineIterator.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/io/StringBuilderWriter.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/truetype/FontFileReader.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/truetype/GlyphTable.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFDirTabEntry.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFMtxEntry.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFTableName.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OpenFont.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFFile.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFGlyphOutputStream.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFOutputStream.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFTableOutputStream.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/util/CharUtilities.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphSequence.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphTester.java [new file with mode: 0644]
fontparser/src/main/java/com/jaredrummler/fontreader/util/ScriptContextTester.java [new file with mode: 0644]
settings.gradle.kts
src/jvmMain/kotlin/info/mechyrdia/lore/fonts.kt

index 4090d4deac0335ef6c3f7613e8e924aa482388ea..08d6d5529a69d153f8b109868f5236fe85171832 100644 (file)
@@ -10,6 +10,7 @@
           <set>
             <option value="$PROJECT_DIR$" />
             <option value="$PROJECT_DIR$/externals" />
+            <option value="$PROJECT_DIR$/fontparser" />
           </set>
         </option>
       </GradleProjectSettings>
index de729a9258690501e60225facb8b7b8820cca251..b2ec0e2f9ac1b50ffae8177d56f968430901f644 100644 (file)
@@ -142,6 +142,7 @@ kotlin {
                                implementation("org.slf4j:slf4j-api:2.0.7")
                                implementation("ch.qos.logback:logback-classic:1.4.14")
                                
+                               implementation(project(":fontparser"))
                                //implementation(project(":fightgame"))
                        }
                }
diff --git a/fontparser/LICENSE b/fontparser/LICENSE
new file mode 100644 (file)
index 0000000..f1c107f
--- /dev/null
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2016 Jared Rummler
+
+   Licensed 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.
\ No newline at end of file
diff --git a/fontparser/build.gradle.kts b/fontparser/build.gradle.kts
new file mode 100644 (file)
index 0000000..ac45f8c
--- /dev/null
@@ -0,0 +1,16 @@
+plugins {
+       java
+}
+
+repositories {
+       mavenCentral()
+}
+
+dependencies {
+}
+
+java {
+       toolchain {
+               languageVersion.set(JavaLanguageVersion.of(17))
+       }
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiClass.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiClass.java
new file mode 100644 (file)
index 0000000..8c36ade
--- /dev/null
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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: License.java 1039179 2010-11-25 21:04:09Z vhennebert $ */
+
+package com.jaredrummler.fontreader.complexscripts.bidi;
+
+import java.util.Arrays;
+
+/**
+ * Bidirectional class utilities.
+ */
+public final class BidiClass {
+
+       private BidiClass() {
+       }
+
+       private static final byte[] BC_L_1 = {
+                       15, 15, 15, 15, 15, 15, 15, 15, 15, 17, 16, 17, 18, 16, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+                       16, 16, 16, 17, 18, 19, 19, 11, 11, 11, 19, 19, 19, 19, 19, 10, 13, 10, 13, 13, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 13,
+                       19, 19, 19, 19, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 19, 19,
+                       19, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 19, 19, 19, 15, 15,
+                       15, 15, 15, 15, 16, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+                       15, 15, 15, 13, 19, 11, 11, 11, 11, 19, 19, 19, 19, 1, 19, 19, 15, 19, 19, 11, 11, 9, 9, 19, 1, 19, 19, 19, 9, 1,
+                       19, 19, 19, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 1, 1, 1, 1, 1, 1,
+                       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 1, 1, 1, 1, 1, 1, 1
+       };
+
+       private static final byte[] BC_R_1 = {
+                       4, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+                       14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 4, 14, 4, 14, 14, 4, 14, 14, 4, 14, 4, 4, 4,
+                       4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+                       4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 12, 12, 12, 12, 5, 5, 19, 19, 5, 11, 11, 5, 13, 5, 19, 19, 14, 14,
+                       14, 14, 14, 14, 14, 14, 14, 14, 14, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+                       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+                       14, 14, 14, 14, 14, 14, 14, 14, 14, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 11, 12, 12, 5, 5, 5, 14, 5, 5, 5, 5,
+                       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+                       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+                       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14, 14, 14, 14, 14, 14, 14, 12, 19, 14, 14, 14, 14,
+                       14, 14, 5, 5, 14, 14, 19, 14, 14, 14, 14, 5, 5, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 5, 5, 5, 5, 5, 5
+       };
+
+       private static final int[] BC_S_1 = {
+                       256, 443, 444, 448, 452, 660, 661, 688, 697, 699, 706, 710, 720, 722, 736, 741, 748, 749, 750, 751, 768, 880, 884,
+                       885, 886, 890, 891, 894, 900, 902, 903, 904, 908, 910, 931, 1014, 1015, 1154, 1155, 1160, 1162, 1329, 1369, 1370,
+                       1377, 1417, 1418, 1792, 1806, 1807, 1808, 1809, 1810, 1840, 1867, 1869, 1958, 1969, 1970, 1984, 1994, 2027, 2036,
+                       2038, 2039, 2042, 2043, 2048, 2070, 2074, 2075, 2084, 2085, 2088, 2089, 2094, 2096, 2111, 2112, 2137, 2140, 2142,
+                       2143, 2304, 2307, 2308, 2362, 2363, 2364, 2365, 2366, 2369, 2377, 2381, 2382, 2384, 2385, 2392, 2402, 2404, 2406,
+                       2416, 2417, 2418, 2425, 2433, 2434, 2437, 2447, 2451, 2474, 2482, 2486, 2492, 2493, 2494, 2497, 2503, 2507, 2509,
+                       2510, 2519, 2524, 2527, 2530, 2534, 2544, 2546, 2548, 2554, 2555, 2561, 2563, 2565, 2575, 2579, 2602, 2610, 2613,
+                       2616, 2620, 2622, 2625, 2631, 2635, 2641, 2649, 2654, 2662, 2672, 2674, 2677, 2689, 2691, 2693, 2703, 2707, 2730,
+                       2738, 2741, 2748, 2749, 2750, 2753, 2759, 2761, 2763, 2765, 2768, 2784, 2786, 2790, 2801, 2817, 2818, 2821, 2831,
+                       2835, 2858, 2866, 2869, 2876, 2877, 2878, 2879, 2880, 2881, 2887, 2891, 2893, 2902, 2903, 2908, 2911, 2914, 2918,
+                       2928, 2929, 2930, 2946, 2947, 2949, 2958, 2962, 2969, 2972, 2974, 2979, 2984, 2990, 3006, 3008, 3009, 3014, 3018,
+                       3021, 3024, 3031, 3046, 3056, 3059, 3065, 3066, 3073, 3077, 3086, 3090, 3114, 3125, 3133, 3134, 3137, 3142, 3146,
+                       3157, 3160, 3168, 3170, 3174, 3192, 3199, 3202, 3205, 3214, 3218, 3242, 3253, 3260, 3261, 3262, 3263, 3264, 3270,
+                       3271, 3274, 3276, 3285, 3294, 3296, 3298, 3302, 3313, 3330, 3333, 3342, 3346, 3389, 3390, 3393, 3398, 3402, 3405,
+                       3406, 3415, 3424, 3426, 3430, 3440, 3449, 3450, 3458, 3461, 3482, 3507, 3517, 3520, 3530, 3535, 3538, 3542, 3544,
+                       3570, 3572, 3585, 3633, 3634, 3636, 3647, 3648, 3654, 3655, 3663, 3664, 3674, 3713, 3716, 3719, 3722, 3725, 3732,
+                       3737, 3745, 3749, 3751, 3754, 3757, 3761, 3762, 3764, 3771, 3773, 3776, 3782, 3784, 3792, 3804, 3840, 3841, 3844,
+                       3859, 3864, 3866, 3872, 3882, 3892, 3893, 3894, 3895, 3896, 3897, 3898, 3899, 3900, 3901, 3902, 3904, 3913, 3953,
+                       3967, 3968, 3973, 3974, 3976, 3981, 3993, 4030, 4038, 4039, 4046, 4048, 4053, 4057, 4096, 4139, 4141, 4145, 4146,
+                       4152, 4153, 4155, 4157, 4159, 4160, 4170, 4176, 4182, 4184, 4186, 4190, 4193, 4194, 4197, 4199, 4206, 4209, 4213,
+                       4226, 4227, 4229, 4231, 4237, 4238, 4239, 4240, 4250, 4253, 4254, 4256, 4304, 4347, 4348, 4352, 4682, 4688, 4696,
+                       4698, 4704, 4746, 4752, 4786, 4792, 4800, 4802, 4808, 4824, 4882, 4888, 4957, 4960, 4961, 4969, 4992, 5008, 5024,
+                       5120, 5121, 5741, 5743, 5760, 5761, 5787, 5788, 5792, 5867, 5870, 5888, 5902, 5906, 5920, 5938, 5941, 5952, 5970,
+                       5984, 5998, 6002, 6016, 6068, 6070, 6071, 6078, 6086, 6087, 6089, 6100, 6103, 6104, 6107, 6108, 6109, 6112, 6128,
+                       6144, 6150, 6151, 6155, 6158, 6160, 6176, 6211, 6212, 6272, 6313, 6314, 6320, 6400, 6432, 6435, 6439, 6441, 6448,
+                       6450, 6451, 6457, 6464, 6468, 6470, 6480, 6512, 6528, 6576, 6593, 6600, 6608, 6618, 6622, 6656, 6679, 6681, 6686,
+                       6688, 6741, 6742, 6743, 6744, 6752, 6753, 6754, 6755, 6757, 6765, 6771, 6783, 6784, 6800, 6816, 6823, 6824, 6912,
+                       6916, 6917, 6964, 6965, 6966, 6971, 6972, 6973, 6978, 6979, 6981, 6992, 7002, 7009, 7019, 7028, 7040, 7042, 7043,
+                       7073, 7074, 7078, 7080, 7082, 7086, 7088, 7104, 7142, 7143, 7144, 7146, 7149, 7150, 7151, 7154, 7164, 7168, 7204,
+                       7212, 7220, 7222, 7227, 7232, 7245, 7248, 7258, 7288, 7294, 7376, 7379, 7380, 7393, 7394, 7401, 7405, 7406, 7410,
+                       7424, 7468, 7522, 7544, 7545, 7579, 7616, 7676, 7680, 7960, 7968, 8008, 8016, 8025, 8027, 8029, 8031, 8064, 8118,
+                       8125, 8126, 8127, 8130, 8134, 8141, 8144, 8150, 8157, 8160, 8173, 8178, 8182, 8189, 8192, 8203, 8206, 8207, 8208,
+                       8214, 8216, 8217, 8218, 8219, 8221, 8222, 8223, 8224, 8232, 8233, 8234, 8235, 8236, 8237, 8238, 8239, 8240, 8245,
+                       8249, 8250, 8251, 8255, 8257, 8260, 8261, 8262, 8263, 8274, 8275, 8276, 8277, 8287, 8288, 8293, 8298, 8304, 8305,
+                       8308, 8314, 8316, 8317, 8318, 8319, 8320, 8330, 8332, 8333, 8334, 8336, 8352, 8400, 8413, 8417, 8418, 8421, 8448,
+                       8450, 8451, 8455, 8456, 8458, 8468, 8469, 8470, 8472, 8473, 8478, 8484, 8485, 8486, 8487, 8488, 8489, 8490, 8494,
+                       8495, 8501, 8505, 8506, 8508, 8512, 8517, 8522, 8523, 8524, 8526, 8527, 8528, 8544, 8579, 8581, 8585, 8592, 8597,
+                       8602, 8604, 8608, 8609, 8611, 8612, 8614, 8615, 8622, 8623, 8654, 8656, 8658, 8659, 8660, 8661, 8692, 8722, 8723,
+                       8724, 8960, 8968, 8972, 8992, 8994, 9001, 9002, 9003, 9014, 9083, 9084, 9085, 9109, 9110, 9115, 9140, 9180, 9186,
+                       9216, 9280, 9312, 9352, 9372, 9450, 9472, 9655, 9656, 9665, 9666, 9720, 9728, 9839, 9840, 9900, 9901, 9985, 10088,
+                       10089, 10090, 10091, 10092, 10093, 10094, 10095, 10096, 10097, 10098, 10099, 10100, 10101, 10102, 10132, 10176,
+                       10181, 10182, 10183, 10188, 10190, 10214, 10215, 10216, 10217, 10218, 10219, 10220, 10221, 10222, 10223, 10224,
+                       10240, 10496, 10627, 10628, 10629, 10630, 10631, 10632, 10633, 10634, 10635, 10636, 10637, 10638, 10639, 10640,
+                       10641, 10642, 10643, 10644, 10645, 10646, 10647, 10648, 10649, 10712, 10713, 10714, 10715, 10716, 10748, 10749,
+                       10750, 11008, 11056, 11077, 11079, 11088, 11264, 11312, 11360, 11389, 11390, 11493, 11499, 11503, 11513, 11517,
+                       11518, 11520, 11568, 11631, 11632, 11647, 11648, 11680, 11688, 11696, 11704, 11712, 11720, 11728, 11736, 11744,
+                       11776, 11778, 11779, 11780, 11781, 11782, 11785, 11786, 11787, 11788, 11789, 11790, 11799, 11800, 11802, 11803,
+                       11804, 11805, 11806, 11808, 11809, 11810, 11811, 11812, 11813, 11814, 11815, 11816, 11817, 11818, 11823, 11824,
+                       11904, 11931, 12032, 12272, 12288, 12289, 12292, 12293, 12294, 12295, 12296, 12297, 12298, 12299, 12300, 12301,
+                       12302, 12303, 12304, 12305, 12306, 12308, 12309, 12310, 12311, 12312, 12313, 12314, 12315, 12316, 12317, 12318,
+                       12320, 12321, 12330, 12336, 12337, 12342, 12344, 12347, 12348, 12349, 12350, 12353, 12441, 12443, 12445, 12447,
+                       12448, 12449, 12539, 12540, 12543, 12549, 12593, 12688, 12690, 12694, 12704, 12736, 12784, 12800, 12829, 12832,
+                       12842, 12880, 12881, 12896, 12924, 12927, 12928, 12938, 12977, 12992, 13004, 13008, 13056, 13175, 13179, 13278,
+                       13280, 13311, 13312, 19904, 19968, 40960, 40981, 40982, 42128, 42192, 42232, 42238, 42240, 42508, 42509, 42512,
+                       42528, 42538, 42560, 42606, 42607, 42608, 42611, 42620, 42622, 42623, 42624, 42656, 42726, 42736, 42738, 42752,
+                       42775, 42784, 42786, 42864, 42865, 42888, 42889, 42891, 42896, 42912, 43002, 43003, 43010, 43011, 43014, 43015,
+                       43019, 43020, 43043, 43045, 43047, 43048, 43056, 43062, 43064, 43065, 43072, 43124, 43136, 43138, 43188, 43204,
+                       43214, 43216, 43232, 43250, 43256, 43259, 43264, 43274, 43302, 43310, 43312, 43335, 43346, 43359, 43360, 43392,
+                       43395, 43396, 43443, 43444, 43446, 43450, 43452, 43453, 43457, 43471, 43472, 43486, 43520, 43561, 43567, 43569,
+                       43571, 43573, 43584, 43587, 43588, 43596, 43597, 43600, 43612, 43616, 43632, 43633, 43639, 43642, 43643, 43648,
+                       43696, 43697, 43698, 43701, 43703, 43705, 43710, 43712, 43713, 43714, 43739, 43741, 43742, 43777, 43785, 43793,
+                       43808, 43816, 43968, 44003, 44005, 44006, 44008, 44009, 44011, 44012, 44013, 44016, 44032, 55216, 55243, 57344,
+                       63744, 64048, 64112, 64256, 64275, 64285, 64286, 64287, 64297, 64298, 64311, 64312, 64317, 64318, 64319, 64320,
+                       64322, 64323, 64325, 64326, 64336, 64434, 64450, 64467, 64830, 64831, 64832, 64848, 64912, 64914, 64968, 64976,
+                       65008, 65020, 65021, 65022, 65024, 65040, 65047, 65048, 65049, 65056, 65072, 65073, 65075, 65077, 65078, 65079,
+                       65080, 65081, 65082, 65083, 65084, 65085, 65086, 65087, 65088, 65089, 65090, 65091, 65092, 65093, 65095, 65096,
+                       65097, 65101, 65104, 65105, 65106, 65108, 65109, 65110, 65112, 65113, 65114, 65115, 65116, 65117, 65118, 65119,
+                       65120, 65122, 65123, 65124, 65128, 65129, 65130, 65131, 65136, 65141, 65142, 65277, 65279, 65281, 65283, 65284,
+                       65285, 65286, 65288, 65289, 65290, 65291, 65292, 65293, 65294, 65296, 65306, 65307, 65308, 65311, 65313, 65339,
+                       65340, 65341, 65342, 65343, 65344, 65345, 65371, 65372, 65373, 65374, 65375, 65376, 65377, 65378, 65379, 65380,
+                       65382, 65392, 65393, 65438, 65440, 65474, 65482, 65490, 65498, 65504, 65506, 65507, 65508, 65509, 65512, 65513,
+                       65517, 65520, 65529, 65532, 65534, 65536, 65549, 65576, 65596, 65599, 65616, 65664, 65792, 65793, 65794, 65799,
+                       65847, 65856, 65909, 65913, 65930, 65936, 66000, 66045, 66176, 66208, 66304, 66336, 66352, 66369, 66370, 66378,
+                       66432, 66463, 66464, 66504, 66512, 66513, 66560, 66640, 66720, 67584, 67590, 67592, 67593, 67594, 67638, 67639,
+                       67641, 67644, 67645, 67647, 67670, 67671, 67672, 67680, 67840, 67862, 67868, 67871, 67872, 67898, 67903, 67904,
+                       68096, 68097, 68100, 68101, 68103, 68108, 68112, 68116, 68117, 68120, 68121, 68148, 68152, 68155, 68159, 68160,
+                       68168, 68176, 68185, 68192, 68221, 68223, 68224, 68352, 68406, 68409, 68416, 68438, 68440, 68448, 68467, 68472,
+                       68480, 68608, 68681, 69216, 69247, 69632, 69633, 69634, 69635, 69688, 69703, 69714, 69734, 69760, 69762, 69763,
+                       69808, 69811, 69815, 69817, 69819, 69821, 69822, 73728, 74752, 74864, 77824, 92160, 110592, 118784, 119040,
+                       119081, 119141, 119143, 119146, 119149, 119155, 119163, 119171, 119173, 119180, 119210, 119214, 119296, 119362,
+                       119365, 119552, 119648, 119808, 119894, 119966, 119970, 119973, 119977, 119982, 119995, 119997, 120005, 120071,
+                       120077, 120086, 120094, 120123, 120128, 120134, 120138, 120146, 120488, 120513, 120514, 120539, 120540, 120571,
+                       120572, 120597, 120598, 120629, 120630, 120655, 120656, 120687, 120688, 120713, 120714, 120745, 120746, 120771,
+                       120772, 120782, 124928, 126976, 127024, 127136, 127153, 127169, 127185, 127232, 127248, 127280, 127344, 127462,
+                       127504, 127552, 127568, 127744, 127792, 127799, 127872, 127904, 127942, 127968, 128000, 128064, 128066, 128140,
+                       128141, 128249, 128256, 128292, 128293, 128336, 128507, 128513, 128530, 128534, 128536, 128538, 128540, 128544,
+                       128552, 128557, 128560, 128565, 128581, 128640, 128768, 131070, 131072, 173824, 177984, 194560, 196606, 262142,
+                       327678, 393214, 458750, 524286, 589822, 655358, 720894, 786430, 851966, 917502, 917505, 917506, 917536, 917632,
+                       917760, 918000, 983038, 983040, 1048574, 1048576, 1114110
+       };
+
+       private static final int[] BC_E_1 = {
+                       442, 443, 447, 451, 659, 660, 687, 696, 698, 705, 709, 719, 721, 735, 740, 747, 748, 749, 750, 767, 879, 883, 884,
+                       885, 887, 890, 893, 894, 901, 902, 903, 906, 908, 929, 1013, 1014, 1153, 1154, 1159, 1161, 1319, 1366, 1369, 1375,
+                       1415, 1417, 1418, 1805, 1806, 1807, 1808, 1809, 1839, 1866, 1868, 1957, 1968, 1969, 1983, 1993, 2026, 2035, 2037,
+                       2038, 2041, 2042, 2047, 2069, 2073, 2074, 2083, 2084, 2087, 2088, 2093, 2095, 2110, 2111, 2136, 2139, 2141, 2142,
+                       2303, 2306, 2307, 2361, 2362, 2363, 2364, 2365, 2368, 2376, 2380, 2381, 2383, 2384, 2391, 2401, 2403, 2405, 2415,
+                       2416, 2417, 2423, 2431, 2433, 2435, 2444, 2448, 2472, 2480, 2482, 2489, 2492, 2493, 2496, 2500, 2504, 2508, 2509,
+                       2510, 2519, 2525, 2529, 2531, 2543, 2545, 2547, 2553, 2554, 2555, 2562, 2563, 2570, 2576, 2600, 2608, 2611, 2614,
+                       2617, 2620, 2624, 2626, 2632, 2637, 2641, 2652, 2654, 2671, 2673, 2676, 2677, 2690, 2691, 2701, 2705, 2728, 2736,
+                       2739, 2745, 2748, 2749, 2752, 2757, 2760, 2761, 2764, 2765, 2768, 2785, 2787, 2799, 2801, 2817, 2819, 2828, 2832,
+                       2856, 2864, 2867, 2873, 2876, 2877, 2878, 2879, 2880, 2884, 2888, 2892, 2893, 2902, 2903, 2909, 2913, 2915, 2927,
+                       2928, 2929, 2935, 2946, 2947, 2954, 2960, 2965, 2970, 2972, 2975, 2980, 2986, 3001, 3007, 3008, 3010, 3016, 3020,
+                       3021, 3024, 3031, 3055, 3058, 3064, 3065, 3066, 3075, 3084, 3088, 3112, 3123, 3129, 3133, 3136, 3140, 3144, 3149,
+                       3158, 3161, 3169, 3171, 3183, 3198, 3199, 3203, 3212, 3216, 3240, 3251, 3257, 3260, 3261, 3262, 3263, 3268, 3270,
+                       3272, 3275, 3277, 3286, 3294, 3297, 3299, 3311, 3314, 3331, 3340, 3344, 3386, 3389, 3392, 3396, 3400, 3404, 3405,
+                       3406, 3415, 3425, 3427, 3439, 3445, 3449, 3455, 3459, 3478, 3505, 3515, 3517, 3526, 3530, 3537, 3540, 3542, 3551,
+                       3571, 3572, 3632, 3633, 3635, 3642, 3647, 3653, 3654, 3662, 3663, 3673, 3675, 3714, 3716, 3720, 3722, 3725, 3735,
+                       3743, 3747, 3749, 3751, 3755, 3760, 3761, 3763, 3769, 3772, 3773, 3780, 3782, 3789, 3801, 3805, 3840, 3843, 3858,
+                       3863, 3865, 3871, 3881, 3891, 3892, 3893, 3894, 3895, 3896, 3897, 3898, 3899, 3900, 3901, 3903, 3911, 3948, 3966,
+                       3967, 3972, 3973, 3975, 3980, 3991, 4028, 4037, 4038, 4044, 4047, 4052, 4056, 4058, 4138, 4140, 4144, 4145, 4151,
+                       4152, 4154, 4156, 4158, 4159, 4169, 4175, 4181, 4183, 4185, 4189, 4192, 4193, 4196, 4198, 4205, 4208, 4212, 4225,
+                       4226, 4228, 4230, 4236, 4237, 4238, 4239, 4249, 4252, 4253, 4255, 4293, 4346, 4347, 4348, 4680, 4685, 4694, 4696,
+                       4701, 4744, 4749, 4784, 4789, 4798, 4800, 4805, 4822, 4880, 4885, 4954, 4959, 4960, 4968, 4988, 5007, 5017, 5108,
+                       5120, 5740, 5742, 5759, 5760, 5786, 5787, 5788, 5866, 5869, 5872, 5900, 5905, 5908, 5937, 5940, 5942, 5969, 5971,
+                       5996, 6000, 6003, 6067, 6069, 6070, 6077, 6085, 6086, 6088, 6099, 6102, 6103, 6106, 6107, 6108, 6109, 6121, 6137,
+                       6149, 6150, 6154, 6157, 6158, 6169, 6210, 6211, 6263, 6312, 6313, 6314, 6389, 6428, 6434, 6438, 6440, 6443, 6449,
+                       6450, 6456, 6459, 6464, 6469, 6479, 6509, 6516, 6571, 6592, 6599, 6601, 6617, 6618, 6655, 6678, 6680, 6683, 6687,
+                       6740, 6741, 6742, 6743, 6750, 6752, 6753, 6754, 6756, 6764, 6770, 6780, 6783, 6793, 6809, 6822, 6823, 6829, 6915,
+                       6916, 6963, 6964, 6965, 6970, 6971, 6972, 6977, 6978, 6980, 6987, 7001, 7008, 7018, 7027, 7036, 7041, 7042, 7072,
+                       7073, 7077, 7079, 7081, 7082, 7087, 7097, 7141, 7142, 7143, 7145, 7148, 7149, 7150, 7153, 7155, 7167, 7203, 7211,
+                       7219, 7221, 7223, 7231, 7241, 7247, 7257, 7287, 7293, 7295, 7378, 7379, 7392, 7393, 7400, 7404, 7405, 7409, 7410,
+                       7467, 7521, 7543, 7544, 7578, 7615, 7654, 7679, 7957, 7965, 8005, 8013, 8023, 8025, 8027, 8029, 8061, 8116, 8124,
+                       8125, 8126, 8129, 8132, 8140, 8143, 8147, 8155, 8159, 8172, 8175, 8180, 8188, 8190, 8202, 8205, 8206, 8207, 8213,
+                       8215, 8216, 8217, 8218, 8220, 8221, 8222, 8223, 8231, 8232, 8233, 8234, 8235, 8236, 8237, 8238, 8239, 8244, 8248,
+                       8249, 8250, 8254, 8256, 8259, 8260, 8261, 8262, 8273, 8274, 8275, 8276, 8286, 8287, 8292, 8297, 8303, 8304, 8305,
+                       8313, 8315, 8316, 8317, 8318, 8319, 8329, 8331, 8332, 8333, 8334, 8348, 8377, 8412, 8416, 8417, 8420, 8432, 8449,
+                       8450, 8454, 8455, 8457, 8467, 8468, 8469, 8471, 8472, 8477, 8483, 8484, 8485, 8486, 8487, 8488, 8489, 8493, 8494,
+                       8500, 8504, 8505, 8507, 8511, 8516, 8521, 8522, 8523, 8525, 8526, 8527, 8543, 8578, 8580, 8584, 8585, 8596, 8601,
+                       8603, 8607, 8608, 8610, 8611, 8613, 8614, 8621, 8622, 8653, 8655, 8657, 8658, 8659, 8660, 8691, 8721, 8722, 8723,
+                       8959, 8967, 8971, 8991, 8993, 9000, 9001, 9002, 9013, 9082, 9083, 9084, 9108, 9109, 9114, 9139, 9179, 9185, 9203,
+                       9254, 9290, 9351, 9371, 9449, 9471, 9654, 9655, 9664, 9665, 9719, 9727, 9838, 9839, 9899, 9900, 9983, 10087,
+                       10088, 10089, 10090, 10091, 10092, 10093, 10094, 10095, 10096, 10097, 10098, 10099, 10100, 10101, 10131, 10175,
+                       10180, 10181, 10182, 10186, 10188, 10213, 10214, 10215, 10216, 10217, 10218, 10219, 10220, 10221, 10222, 10223,
+                       10239, 10495, 10626, 10627, 10628, 10629, 10630, 10631, 10632, 10633, 10634, 10635, 10636, 10637, 10638, 10639,
+                       10640, 10641, 10642, 10643, 10644, 10645, 10646, 10647, 10648, 10711, 10712, 10713, 10714, 10715, 10747, 10748,
+                       10749, 11007, 11055, 11076, 11078, 11084, 11097, 11310, 11358, 11388, 11389, 11492, 11498, 11502, 11505, 11516,
+                       11517, 11519, 11557, 11621, 11631, 11632, 11647, 11670, 11686, 11694, 11702, 11710, 11718, 11726, 11734, 11742,
+                       11775, 11777, 11778, 11779, 11780, 11781, 11784, 11785, 11786, 11787, 11788, 11789, 11798, 11799, 11801, 11802,
+                       11803, 11804, 11805, 11807, 11808, 11809, 11810, 11811, 11812, 11813, 11814, 11815, 11816, 11817, 11822, 11823,
+                       11825, 11929, 12019, 12245, 12283, 12288, 12291, 12292, 12293, 12294, 12295, 12296, 12297, 12298, 12299, 12300,
+                       12301, 12302, 12303, 12304, 12305, 12307, 12308, 12309, 12310, 12311, 12312, 12313, 12314, 12315, 12316, 12317,
+                       12319, 12320, 12329, 12335, 12336, 12341, 12343, 12346, 12347, 12348, 12349, 12351, 12438, 12442, 12444, 12446,
+                       12447, 12448, 12538, 12539, 12542, 12543, 12589, 12686, 12689, 12693, 12703, 12730, 12771, 12799, 12828, 12830,
+                       12841, 12879, 12880, 12895, 12923, 12926, 12927, 12937, 12976, 12991, 13003, 13007, 13054, 13174, 13178, 13277,
+                       13279, 13310, 13311, 19893, 19967, 40907, 40980, 40981, 42124, 42182, 42231, 42237, 42239, 42507, 42508, 42511,
+                       42527, 42537, 42539, 42605, 42606, 42607, 42610, 42611, 42621, 42622, 42623, 42647, 42725, 42735, 42737, 42743,
+                       42774, 42783, 42785, 42863, 42864, 42887, 42888, 42890, 42894, 42897, 42921, 43002, 43009, 43010, 43013, 43014,
+                       43018, 43019, 43042, 43044, 43046, 43047, 43051, 43061, 43063, 43064, 43065, 43123, 43127, 43137, 43187, 43203,
+                       43204, 43215, 43225, 43249, 43255, 43258, 43259, 43273, 43301, 43309, 43311, 43334, 43345, 43347, 43359, 43388,
+                       43394, 43395, 43442, 43443, 43445, 43449, 43451, 43452, 43456, 43469, 43471, 43481, 43487, 43560, 43566, 43568,
+                       43570, 43572, 43574, 43586, 43587, 43595, 43596, 43597, 43609, 43615, 43631, 43632, 43638, 43641, 43642, 43643,
+                       43695, 43696, 43697, 43700, 43702, 43704, 43709, 43711, 43712, 43713, 43714, 43740, 43741, 43743, 43782, 43790,
+                       43798, 43814, 43822, 44002, 44004, 44005, 44007, 44008, 44010, 44011, 44012, 44013, 44025, 55203, 55238, 55291,
+                       63743, 64045, 64109, 64217, 64262, 64279, 64285, 64286, 64296, 64297, 64310, 64311, 64316, 64317, 64318, 64319,
+                       64321, 64322, 64324, 64325, 64335, 64433, 64449, 64466, 64829, 64830, 64831, 64847, 64911, 64913, 64967, 64975,
+                       65007, 65019, 65020, 65021, 65023, 65039, 65046, 65047, 65048, 65049, 65062, 65072, 65074, 65076, 65077, 65078,
+                       65079, 65080, 65081, 65082, 65083, 65084, 65085, 65086, 65087, 65088, 65089, 65090, 65091, 65092, 65094, 65095,
+                       65096, 65100, 65103, 65104, 65105, 65106, 65108, 65109, 65111, 65112, 65113, 65114, 65115, 65116, 65117, 65118,
+                       65119, 65121, 65122, 65123, 65126, 65128, 65129, 65130, 65131, 65140, 65141, 65276, 65278, 65279, 65282, 65283,
+                       65284, 65285, 65287, 65288, 65289, 65290, 65291, 65292, 65293, 65295, 65305, 65306, 65307, 65310, 65312, 65338,
+                       65339, 65340, 65341, 65342, 65343, 65344, 65370, 65371, 65372, 65373, 65374, 65375, 65376, 65377, 65378, 65379,
+                       65381, 65391, 65392, 65437, 65439, 65470, 65479, 65487, 65495, 65500, 65505, 65506, 65507, 65508, 65510, 65512,
+                       65516, 65518, 65528, 65531, 65533, 65535, 65547, 65574, 65594, 65597, 65613, 65629, 65786, 65792, 65793, 65794,
+                       65843, 65855, 65908, 65912, 65929, 65930, 65947, 66044, 66045, 66204, 66256, 66334, 66339, 66368, 66369, 66377,
+                       66378, 66461, 66463, 66499, 66511, 66512, 66517, 66639, 66717, 66729, 67589, 67591, 67592, 67593, 67637, 67638,
+                       67640, 67643, 67644, 67646, 67669, 67670, 67671, 67679, 67839, 67861, 67867, 67870, 67871, 67897, 67902, 67903,
+                       68095, 68096, 68099, 68100, 68102, 68107, 68111, 68115, 68116, 68119, 68120, 68147, 68151, 68154, 68158, 68159,
+                       68167, 68175, 68184, 68191, 68220, 68222, 68223, 68351, 68405, 68408, 68415, 68437, 68439, 68447, 68466, 68471,
+                       68479, 68607, 68680, 69215, 69246, 69631, 69632, 69633, 69634, 69687, 69702, 69709, 69733, 69743, 69761, 69762,
+                       69807, 69810, 69814, 69816, 69818, 69820, 69821, 69825, 74606, 74850, 74867, 78894, 92728, 110593, 119029, 119078,
+                       119140, 119142, 119145, 119148, 119154, 119162, 119170, 119172, 119179, 119209, 119213, 119261, 119361, 119364,
+                       119365, 119638, 119665, 119892, 119964, 119967, 119970, 119974, 119980, 119993, 119995, 120003, 120069, 120074,
+                       120084, 120092, 120121, 120126, 120132, 120134, 120144, 120485, 120512, 120513, 120538, 120539, 120570, 120571,
+                       120596, 120597, 120628, 120629, 120654, 120655, 120686, 120687, 120712, 120713, 120744, 120745, 120770, 120771,
+                       120779, 120831, 126975, 127019, 127123, 127150, 127166, 127183, 127199, 127242, 127278, 127337, 127386, 127490,
+                       127546, 127560, 127569, 127776, 127797, 127868, 127891, 127940, 127946, 127984, 128062, 128064, 128139, 128140,
+                       128247, 128252, 128291, 128292, 128317, 128359, 128511, 128528, 128532, 128534, 128536, 128538, 128542, 128549,
+                       128555, 128557, 128563, 128576, 128591, 128709, 128883, 131071, 173782, 177972, 178205, 195101, 196607, 262143,
+                       327679, 393215, 458751, 524287, 589823, 655359, 720895, 786431, 851967, 917504, 917505, 917535, 917631, 917759,
+                       917999, 921599, 983039, 1048573, 1048575, 1114109, 1114111
+       };
+
+       private static final byte[] BC_C_1 = {
+                       1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 19, 19, 1, 19, 1, 19, 19, 19, 1, 19, 14, 1, 19, 19, 1, 1, 1, 19, 19, 1, 19, 1, 1,
+                       1, 1, 19, 1, 1, 14, 14, 1, 1, 1, 1, 1, 1, 19, 5, 5, 12, 5, 14, 5, 14, 5, 5, 14, 5, 5, 4, 4, 14, 4, 19, 19, 4, 4,
+                       4, 14, 4, 14, 4, 14, 4, 14, 4, 4, 4, 4, 14, 4, 4, 4, 14, 1, 1, 14, 1, 14, 1, 1, 14, 1, 14, 1, 1, 14, 1, 14, 1, 1,
+                       1, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 14, 1, 1, 14, 1, 1, 1, 1, 14, 1, 1, 11, 1, 1, 11, 14, 1, 1, 1, 1,
+                       1, 1, 1, 1, 14, 1, 14, 14, 14, 14, 1, 1, 1, 14, 1, 14, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 14, 14, 1, 1, 14, 1, 1,
+                       14, 1, 11, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 14, 1, 14, 1, 1, 14, 14, 1, 1, 1, 14, 1, 1, 1, 1, 14, 1, 1, 1, 1, 1,
+                       1, 1, 1, 1, 1, 1, 14, 1, 1, 1, 14, 1, 1, 1, 1, 19, 11, 19, 1, 1, 1, 1, 1, 1, 1, 14, 1, 14, 14, 14, 1, 1, 14, 1,
+                       19, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 14, 1, 1, 1,
+                       14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 14, 1, 14, 14, 1, 1, 1, 1, 14, 1, 14, 11, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                       1, 1, 1, 1, 1, 1, 14, 1, 14, 14, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 14, 1, 1, 1, 1, 14, 1, 14, 1, 14, 19, 19, 19, 19,
+                       1, 1, 1, 14, 1, 14, 1, 14, 1, 14, 14, 1, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 14, 1,
+                       14, 1, 1, 1, 1, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                       1, 1, 1, 14, 1, 1, 1, 1, 19, 1, 19, 1, 1, 1, 18, 1, 19, 19, 1, 1, 1, 1, 1, 14, 1, 14, 1, 1, 14, 1, 1, 14, 1, 1, 1,
+                       14, 1, 14, 1, 14, 1, 1, 1, 11, 1, 14, 1, 19, 19, 19, 19, 14, 18, 1, 1, 1, 1, 1, 14, 1, 1, 1, 14, 1, 14, 1, 1, 14,
+                       1, 14, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 14, 1, 1, 1, 1, 14, 1, 14, 14, 1, 14, 1, 14, 1, 14, 14, 1, 1, 1,
+                       1, 1, 14, 1, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 14, 1, 14, 1, 1, 1, 14, 1, 14, 1, 1, 1, 1, 14, 1, 14, 1,
+                       14, 1, 14, 1, 1, 1, 1, 14, 1, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 14, 14, 1,
+                       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 19, 1, 1, 19, 1, 1, 19, 1, 19, 1, 1, 19, 18, 15, 1, 4, 19, 19, 19, 19, 19,
+                       19, 19, 19, 19, 19, 18, 16, 2, 6, 8, 3, 7, 13, 11, 19, 19, 19, 19, 19, 19, 13, 19, 19, 19, 19, 19, 19, 19, 18, 15,
+                       15, 15, 9, 1, 9, 10, 19, 19, 19, 1, 9, 10, 19, 19, 19, 1, 11, 14, 14, 14, 14, 14, 19, 1, 19, 1, 19, 1, 19, 1, 19,
+                       19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 11, 1, 1, 1, 19, 1, 19, 1, 19, 19, 19, 1, 1, 19, 1, 1, 1, 19, 19, 19, 19, 19,
+                       19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 10, 11, 19, 19, 19, 19, 19, 19, 19, 19, 19, 1, 19, 19,
+                       19, 1, 19, 19, 19, 19, 19, 19, 19, 19, 9, 1, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 1, 19, 19, 19, 19, 19, 19,
+                       19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+                       19, 1, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+                       19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 1, 1, 1, 1, 1, 19, 1, 14, 19, 19, 19, 1, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1,
+                       1, 1, 14, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+                       19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 18, 19, 19, 1, 1, 1, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+                       19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 1, 14, 19, 1, 19, 1, 1, 1, 19, 19, 1, 14, 19, 1, 1, 19, 1, 19, 1, 1, 1, 1,
+                       1, 1, 1, 1, 19, 1, 1, 19, 1, 1, 19, 19, 1, 19, 1, 1, 1, 19, 1, 19, 1, 1, 19, 1, 19, 1, 19, 1, 19, 1, 1, 1, 1, 19,
+                       1, 1, 1, 1, 1, 19, 1, 1, 1, 1, 1, 14, 14, 19, 14, 19, 19, 1, 1, 1, 14, 1, 19, 19, 19, 1, 1, 1, 19, 1, 1, 1, 1, 1,
+                       1, 14, 1, 14, 1, 14, 1, 1, 14, 1, 19, 1, 1, 11, 11, 1, 19, 1, 1, 1, 14, 1, 1, 14, 1, 1, 1, 1, 1, 14, 1, 1, 14, 1,
+                       1, 1, 14, 1, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                       14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 14, 1, 14, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1,
+                       1, 1, 4, 14, 4, 10, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 19, 19, 5, 5, 5, 5, 5, 15, 5, 5, 19, 5, 14, 19,
+                       19, 19, 19, 14, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+                       13, 19, 13, 19, 13, 19, 19, 19, 19, 19, 19, 19, 19, 11, 19, 10, 10, 19, 19, 11, 11, 19, 5, 5, 5, 5, 15, 19, 11,
+                       11, 11, 19, 19, 19, 19, 10, 13, 10, 13, 9, 13, 19, 19, 19, 1, 19, 19, 19, 19, 19, 19, 1, 19, 19, 19, 19, 19, 19,
+                       19, 19, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 19, 19, 19, 11, 19, 19, 19, 15, 19, 19, 15, 1, 1, 1, 1, 1, 1, 1, 1,
+                       19, 1, 1, 1, 19, 19, 19, 19, 19, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4, 4, 4, 4, 4, 4, 4,
+                       4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 19, 4, 4, 4, 4, 4, 14, 4, 14, 4, 14, 4, 4, 4, 4, 4, 4, 14, 4, 14, 4, 4, 4, 4, 4, 4,
+                       4, 4, 4, 4, 19, 4, 4, 4, 4, 4, 4, 4, 4, 4, 12, 4, 1, 14, 1, 1, 14, 1, 19, 1, 14, 1, 1, 1, 14, 1, 14, 1, 1, 1, 1,
+                       1, 1, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 15, 14, 1, 14, 1, 14, 1, 19, 14, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 1, 1, 19, 1, 1, 1, 19, 1, 1, 1, 19, 1, 1, 1, 19, 1, 9, 4, 19, 19, 19, 19,
+                       19, 19, 9, 1, 1, 1, 1, 1, 1, 1, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 1, 19, 19, 19, 1, 19, 19, 19, 19, 19, 19,
+                       19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 15, 1, 1, 1, 1, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+                       15, 15, 15, 14, 15, 15, 1, 15, 1, 15
+       };
+
+       /**
+        * Lookup bidi class for character expressed as unicode scalar value.
+        *
+        * @param ch a unicode scalar value
+        * @return bidi class
+        */
+       public static int getBidiClass(int ch) {
+               if (ch <= 0x00FF) {
+                       return BC_L_1[ch - 0x0000];
+               } else if ((ch >= 0x0590) && (ch <= 0x06FF)) {
+                       return BC_R_1[ch - 0x0590];
+               } else {
+                       return getBidiClass(ch, BC_S_1, BC_E_1, BC_C_1);
+               }
+       }
+
+       private static int getBidiClass(int ch, int[] sa, int[] ea, byte[] ca) {
+               int k = Arrays.binarySearch(sa, ch);
+               if (k >= 0) {
+                       return ca[k];
+               } else {
+                       k = -(k + 1);
+                       if (k == 0) {
+                               return BidiConstants.L;
+                       } else if (ch <= ea[k - 1]) {
+                               return ca[k - 1];
+                       } else {
+                               return BidiConstants.L;
+                       }
+               }
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiConstants.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiConstants.java
new file mode 100644 (file)
index 0000000..cfc7d72
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.bidi;
+
+/**
+ * <p>Constants used for bidirectional processing.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface BidiConstants {
+
+       /**
+        * first external (official) category
+        */
+       int FIRST = 1;
+
+       // strong category
+
+       /**
+        * left-to-right class
+        */
+       int L = 1;
+
+       /**
+        * left-to-right embedding class
+        */
+       int LRE = 2;
+
+       /**
+        * left-to-right override class
+        */
+       int LRO = 3;
+
+       /**
+        * right-to-left  class
+        */
+       int R = 4;
+
+       /**
+        * right-to-left arabic class
+        */
+       int AL = 5;
+
+       /**
+        * right-to-left embedding class
+        */
+       int RLE = 6;
+
+       /**
+        * right-to-left override class
+        */
+       int RLO = 7;
+
+       // weak category
+
+       /**
+        * pop directional formatting class
+        */
+       int PDF = 8;
+
+       /**
+        * european number class
+        */
+       int EN = 9;
+
+       /**
+        * european number separator class
+        */
+       int ES = 10;
+
+       /**
+        * european number terminator class
+        */
+       int ET = 11;
+
+       /**
+        * arabic number class
+        */
+       int AN = 12;
+
+       /**
+        * common number separator class
+        */
+       int CS = 13;
+
+       /**
+        * non-spacing mark class
+        */
+       int NSM = 14;
+
+       /**
+        * boundary neutral class
+        */
+       int BN = 15;
+
+       // neutral category
+
+       /**
+        * paragraph separator class
+        */
+       int B = 16;
+
+       /**
+        * segment separator class
+        */
+       int S = 17;
+
+       /**
+        * whitespace class
+        */
+       int WS = 18;
+
+       /**
+        * other neutrals class
+        */
+       int ON = 19;
+
+       /**
+        * last external (official) category
+        */
+       int LAST = 19;
+
+       // implementation specific categories
+
+       /**
+        * placeholder for low surrogate
+        */
+       int SURROGATE = 20;
+
+       // other constants
+
+       /**
+        * last
+        * /** maximum bidirectional levels
+        */
+       int MAX_LEVELS = 61;
+
+       /**
+        * override flag
+        */
+       int OVERRIDE = 128;
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/AdvancedTypographicTableFormatException.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/AdvancedTypographicTableFormatException.java
new file mode 100644 (file)
index 0000000..d790578
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+/**
+ * <p>Exception thrown when attempting to decode a truetype font file and a format
+ * constraint is violated.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class AdvancedTypographicTableFormatException extends RuntimeException {
+
+       /**
+        * Instantiate ATT format exception.
+        */
+       public AdvancedTypographicTableFormatException() {
+               super();
+       }
+
+       /**
+        * Instantiate ATT format exception.
+        *
+        * @param message a message string
+        */
+       public AdvancedTypographicTableFormatException(String message) {
+               super(message);
+       }
+
+       /**
+        * Instantiate ATT format exception.
+        *
+        * @param message a message string
+        * @param cause   a <code>Throwable</code> that caused this exception
+        */
+       public AdvancedTypographicTableFormatException(String message, Throwable cause) {
+               super(message, cause);
+       }
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassMapping.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassMapping.java
new file mode 100644 (file)
index 0000000..1d59d22
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+/**
+ * <p>The <code>GlyphClassMapping</code> interface provides glyph identifier to class
+ * index mapping support.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface GlyphClassMapping {
+
+       /**
+        * Obtain size of class table, i.e., ciMax + 1, where ciMax is the maximum
+        * class index.
+        *
+        * @param set for coverage set based class mappings, indicates set index, otherwise ignored
+        * @return size of class table
+        */
+       int getClassSize(int set);
+
+       /**
+        * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of
+        * the class table.
+        *
+        * @param gid glyph identifier (code)
+        * @param set for coverage set based class mappings, indicates set index, otherwise ignored
+        * @return non-negative glyph class index or -1 if glyph identifiers is not mapped by table
+        */
+       int getClassIndex(int gid, int set);
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassTable.java
new file mode 100644 (file)
index 0000000..4d7ed9a
--- /dev/null
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * <p>Base class implementation of glyph class table.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public final class GlyphClassTable extends GlyphMappingTable implements GlyphClassMapping {
+
+       /**
+        * empty mapping table
+        */
+       public static final int GLYPH_CLASS_TYPE_EMPTY = GLYPH_MAPPING_TYPE_EMPTY;
+
+       /**
+        * mapped mapping table
+        */
+       public static final int GLYPH_CLASS_TYPE_MAPPED = GLYPH_MAPPING_TYPE_MAPPED;
+
+       /**
+        * range based mapping table
+        */
+       public static final int GLYPH_CLASS_TYPE_RANGE = GLYPH_MAPPING_TYPE_RANGE;
+
+       /**
+        * empty mapping table
+        */
+       public static final int GLYPH_CLASS_TYPE_COVERAGE_SET = 3;
+
+       private GlyphClassMapping cm;
+
+       private GlyphClassTable(GlyphClassMapping cm) {
+               assert cm != null;
+               assert cm instanceof GlyphMappingTable;
+               this.cm = cm;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int getType() {
+               return ((GlyphMappingTable) cm).getType();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public List getEntries() {
+               return ((GlyphMappingTable) cm).getEntries();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int getClassSize(int set) {
+               return cm.getClassSize(set);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int getClassIndex(int gid, int set) {
+               return cm.getClassIndex(gid, set);
+       }
+
+       /**
+        * Create glyph class table.
+        *
+        * @param entries list of mapped or ranged class entries, or null or empty list
+        * @return a new covera table instance
+        */
+       public static GlyphClassTable createClassTable(List entries) {
+               GlyphClassMapping cm;
+               if ((entries == null) || (entries.size() == 0)) {
+                       cm = new EmptyClassTable(entries);
+               } else if (isMappedClass(entries)) {
+                       cm = new MappedClassTable(entries);
+               } else if (isRangeClass(entries)) {
+                       cm = new RangeClassTable(entries);
+               } else if (isCoverageSetClass(entries)) {
+                       cm = new CoverageSetClassTable(entries);
+               } else {
+                       cm = null;
+               }
+               assert cm != null : "unknown class type";
+               return new GlyphClassTable(cm);
+       }
+
+       private static boolean isMappedClass(List entries) {
+               if ((entries == null) || (entries.size() == 0)) {
+                       return false;
+               } else {
+                       for (Iterator it = entries.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (!(o instanceof Integer)) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+       }
+
+       private static boolean isRangeClass(List entries) {
+               if ((entries == null) || (entries.size() == 0)) {
+                       return false;
+               } else {
+                       for (Iterator it = entries.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (!(o instanceof MappingRange)) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+       }
+
+       private static boolean isCoverageSetClass(List entries) {
+               if ((entries == null) || (entries.size() == 0)) {
+                       return false;
+               } else {
+                       for (Iterator it = entries.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (!(o instanceof GlyphCoverageTable)) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+       }
+
+       private static class EmptyClassTable extends GlyphMappingTable.EmptyMappingTable implements GlyphClassMapping {
+
+               public EmptyClassTable(List entries) {
+                       super(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getClassSize(int set) {
+                       return 0;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getClassIndex(int gid, int set) {
+                       return -1;
+               }
+       }
+
+       private static class MappedClassTable extends GlyphMappingTable.MappedMappingTable implements GlyphClassMapping {
+
+               private int firstGlyph;
+               private int[] gca;
+               private int gcMax = -1;
+
+               public MappedClassTable(List entries) {
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       List entries = new java.util.ArrayList();
+                       entries.add(Integer.valueOf(firstGlyph));
+                       if (gca != null) {
+                               for (int i = 0, n = gca.length; i < n; i++) {
+                                       entries.add(Integer.valueOf(gca[i]));
+                               }
+                       }
+                       return entries;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMappingSize() {
+                       return gcMax + 1;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMappedIndex(int gid) {
+                       int i = gid - firstGlyph;
+                       if ((i >= 0) && (i < gca.length)) {
+                               return gca[i];
+                       } else {
+                               return -1;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getClassSize(int set) {
+                       return getMappingSize();
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getClassIndex(int gid, int set) {
+                       return getMappedIndex(gid);
+               }
+
+               private void populate(List entries) {
+                       // obtain entries iterator
+                       Iterator it = entries.iterator();
+                       // extract first glyph
+                       int firstGlyph = 0;
+                       if (it.hasNext()) {
+                               Object o = it.next();
+                               if (o instanceof Integer) {
+                                       firstGlyph = ((Integer) o).intValue();
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entry, first entry must be Integer denoting first glyph value, but is: " + o);
+                               }
+                       }
+                       // extract glyph class array
+                       int i = 0;
+                       int n = entries.size() - 1;
+                       int gcMax = -1;
+                       int[] gca = new int[n];
+                       while (it.hasNext()) {
+                               Object o = it.next();
+                               if (o instanceof Integer) {
+                                       int gc = ((Integer) o).intValue();
+                                       gca[i++] = gc;
+                                       if (gc > gcMax) {
+                                               gcMax = gc;
+                                       }
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException("illegal mapping entry, must be Integer: " + o);
+                               }
+                       }
+                       assert i == n;
+                       assert this.gca == null;
+                       this.firstGlyph = firstGlyph;
+                       this.gca = gca;
+                       this.gcMax = gcMax;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{ firstGlyph = " + firstGlyph + ", classes = {");
+                       for (int i = 0, n = gca.length; i < n; i++) {
+                               if (i > 0) {
+                                       sb.append(',');
+                               }
+                               sb.append(Integer.toString(gca[i]));
+                       }
+                       sb.append("} }");
+                       return sb.toString();
+               }
+       }
+
+       private static class RangeClassTable extends GlyphMappingTable.RangeMappingTable implements GlyphClassMapping {
+
+               public RangeClassTable(List entries) {
+                       super(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMappedIndex(int gid, int s, int m) {
+                       return m;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getClassSize(int set) {
+                       return getMappingSize();
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getClassIndex(int gid, int set) {
+                       return getMappedIndex(gid);
+               }
+       }
+
+       private static class CoverageSetClassTable extends GlyphMappingTable.EmptyMappingTable implements GlyphClassMapping {
+
+               public CoverageSetClassTable(List entries) {
+                       throw new UnsupportedOperationException("coverage set class table not yet supported");
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GLYPH_CLASS_TYPE_COVERAGE_SET;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getClassSize(int set) {
+                       return 0;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getClassIndex(int gid, int set) {
+                       return -1;
+               }
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphContextTester.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphContextTester.java
new file mode 100644 (file)
index 0000000..3ae3833
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import com.jaredrummler.fontreader.util.GlyphSequence;
+
+/**
+ * <p>Interface for testing the originating (source) character context of a glyph sequence.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface GlyphContextTester {
+
+       /**
+        * Perform a test on a glyph sequence in a specific (originating) character context.
+        *
+        * @param script   governing script
+        * @param language governing language
+        * @param feature  governing feature
+        * @param gs       glyph sequence to test
+        * @param index    index into glyph sequence to test
+        * @param flags    that apply to lookup in scope
+        * @return true if test is satisfied
+        */
+       boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags);
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageMapping.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageMapping.java
new file mode 100644 (file)
index 0000000..2a28895
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+/**
+ * <p>The <code>GlyphCoverageMapping</code> interface provides glyph identifier to coverage
+ * index mapping support.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface GlyphCoverageMapping {
+
+       /**
+        * Obtain size of coverage table, i.e., ciMax + 1, where ciMax is the maximum
+        * coverage index.
+        *
+        * @return size of coverage table
+        */
+       int getCoverageSize();
+
+       /**
+        * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of
+        * the coverage table.
+        *
+        * @param gid glyph identifier (code)
+        * @return non-negative glyph coverage index or -1 if glyph identifiers is not mapped by table
+        */
+       int getCoverageIndex(int gid);
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageTable.java
new file mode 100644 (file)
index 0000000..eb682f2
--- /dev/null
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * <p>.Base class implementation of glyph coverage table.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public final class GlyphCoverageTable extends GlyphMappingTable implements GlyphCoverageMapping {
+
+       /**
+        * empty mapping table
+        */
+       public static final int GLYPH_COVERAGE_TYPE_EMPTY = GLYPH_MAPPING_TYPE_EMPTY;
+
+       /**
+        * mapped mapping table
+        */
+       public static final int GLYPH_COVERAGE_TYPE_MAPPED = GLYPH_MAPPING_TYPE_MAPPED;
+
+       /**
+        * range based mapping table
+        */
+       public static final int GLYPH_COVERAGE_TYPE_RANGE = GLYPH_MAPPING_TYPE_RANGE;
+
+       private GlyphCoverageMapping cm;
+
+       private GlyphCoverageTable(GlyphCoverageMapping cm) {
+               this.cm = cm;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int getType() {
+               return ((GlyphMappingTable) cm).getType();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public List getEntries() {
+               return ((GlyphMappingTable) cm).getEntries();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int getCoverageSize() {
+               return cm.getCoverageSize();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int getCoverageIndex(int gid) {
+               return cm.getCoverageIndex(gid);
+       }
+
+       /**
+        * Create glyph coverage table.
+        *
+        * @param entries list of mapped or ranged coverage entries, or null or empty list
+        * @return a new covera table instance
+        */
+       public static GlyphCoverageTable createCoverageTable(List entries) {
+               GlyphCoverageMapping cm;
+               if ((entries == null) || (entries.size() == 0)) {
+                       cm = new EmptyCoverageTable(entries);
+               } else if (isMappedCoverage(entries)) {
+                       cm = new MappedCoverageTable(entries);
+               } else if (isRangeCoverage(entries)) {
+                       cm = new RangeCoverageTable(entries);
+               } else {
+                       cm = null;
+               }
+               assert cm != null : "unknown coverage type";
+               return new GlyphCoverageTable(cm);
+       }
+
+       private static boolean isMappedCoverage(List entries) {
+               if ((entries == null) || (entries.size() == 0)) {
+                       return false;
+               } else {
+                       for (Iterator it = entries.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (!(o instanceof Integer)) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+       }
+
+       private static boolean isRangeCoverage(List entries) {
+               if ((entries == null) || (entries.size() == 0)) {
+                       return false;
+               } else {
+                       for (Iterator it = entries.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (!(o instanceof MappingRange)) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+       }
+
+       private static class EmptyCoverageTable extends GlyphMappingTable.EmptyMappingTable implements GlyphCoverageMapping {
+
+               public EmptyCoverageTable(List entries) {
+                       super(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getCoverageSize() {
+                       return 0;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getCoverageIndex(int gid) {
+                       return -1;
+               }
+       }
+
+       private static class MappedCoverageTable extends GlyphMappingTable.MappedMappingTable
+                       implements GlyphCoverageMapping {
+
+               private int[] map;
+
+               public MappedCoverageTable(List entries) {
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       List entries = new java.util.ArrayList();
+                       if (map != null) {
+                               for (int i = 0, n = map.length; i < n; i++) {
+                                       entries.add(Integer.valueOf(map[i]));
+                               }
+                       }
+                       return entries;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMappingSize() {
+                       return (map != null) ? map.length : 0;
+               }
+
+               public int getMappedIndex(int gid) {
+                       int i;
+                       if ((i = Arrays.binarySearch(map, gid)) >= 0) {
+                               return i;
+                       } else {
+                               return -1;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getCoverageSize() {
+                       return getMappingSize();
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getCoverageIndex(int gid) {
+                       return getMappedIndex(gid);
+               }
+
+               private void populate(List entries) {
+                       int i = 0;
+                       int skipped = 0;
+                       int n = entries.size();
+                       int gidMax = -1;
+                       int[] map = new int[n];
+                       for (Iterator it = entries.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (o instanceof Integer) {
+                                       int gid = ((Integer) o).intValue();
+                                       if ((gid >= 0) && (gid < 65536)) {
+                                               if (gid > gidMax) {
+                                                       map[i++] = gidMax = gid;
+                                               } else {
+                                                       skipped++;
+                                               }
+                                       } else {
+                                               throw new AdvancedTypographicTableFormatException("illegal glyph index: " + gid);
+                                       }
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException("illegal coverage entry, must be Integer: " + o);
+                               }
+                       }
+                       assert (i + skipped) == n;
+                       assert this.map == null;
+                       this.map = map;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append('{');
+                       for (int i = 0, n = map.length; i < n; i++) {
+                               if (i > 0) {
+                                       sb.append(',');
+                               }
+                               sb.append(Integer.toString(map[i]));
+                       }
+                       sb.append('}');
+                       return sb.toString();
+               }
+       }
+
+       private static class RangeCoverageTable extends GlyphMappingTable.RangeMappingTable implements GlyphCoverageMapping {
+
+               public RangeCoverageTable(List entries) {
+                       super(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMappedIndex(int gid, int s, int m) {
+                       return m + gid - s;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getCoverageSize() {
+                       return getMappingSize();
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getCoverageIndex(int gid) {
+                       return getMappedIndex(gid);
+               }
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinition.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinition.java
new file mode 100644 (file)
index 0000000..f7c510f
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+/**
+ * <p>The <code>GlyphDefinition</code> interface is a marker interface implemented by a glyph definition
+ * subtable.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface GlyphDefinition {
+
+       /**
+        * Determine if some definition is available for a specific glyph.
+        *
+        * @param gi a glyph index
+        * @return true if some (unspecified) definition is available for the specified glyph
+        */
+       boolean hasDefinition(int gi);
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionSubtable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionSubtable.java
new file mode 100644 (file)
index 0000000..aaf5fc7
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import com.jaredrummler.fontreader.fonts.GlyphSubtable;
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+
+/**
+ * <p>The <code>GlyphDefinitionSubtable</code> implements an abstract base of a glyph definition subtable,
+ * providing a default implementation of the <code>GlyphDefinition</code> interface.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public abstract class GlyphDefinitionSubtable extends GlyphSubtable implements GlyphDefinition {
+
+       /**
+        * Instantiate a <code>GlyphDefinitionSubtable</code>.
+        *
+        * @param id       subtable identifier
+        * @param sequence subtable sequence
+        * @param flags    subtable flags
+        * @param format   subtable format
+        * @param mapping  subtable coverage table
+        */
+       protected GlyphDefinitionSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping) {
+               super(id, sequence, flags, format, mapping);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int getTableType() {
+               return GlyphTable.GLYPH_TABLE_TYPE_DEFINITION;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String getTypeName() {
+               return GlyphDefinitionTable.getLookupTypeName(getType());
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean usesReverseScan() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean hasDefinition(int gi) {
+               GlyphCoverageMapping cvm;
+               if ((cvm = getCoverage()) != null) {
+                       if (cvm.getCoverageIndex(gi) >= 0) {
+                               return true;
+                       }
+               }
+               GlyphClassMapping clm;
+               if ((clm = getClasses()) != null) {
+                       if (clm.getClassIndex(gi, 0) >= 0) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionTable.java
new file mode 100644 (file)
index 0000000..5c8b6a9
--- /dev/null
@@ -0,0 +1,548 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import com.jaredrummler.fontreader.complexscripts.scripts.ScriptProcessor;
+import com.jaredrummler.fontreader.fonts.GlyphSubtable;
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * <p>The <code>GlyphDefinitionTable</code> class is a glyph table that implements
+ * glyph definition functionality according to the OpenType GDEF table.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GlyphDefinitionTable extends GlyphTable {
+
+       /**
+        * glyph class subtable type
+        */
+       public static final int GDEF_LOOKUP_TYPE_GLYPH_CLASS = 1;
+       /**
+        * attachment point subtable type
+        */
+       public static final int GDEF_LOOKUP_TYPE_ATTACHMENT_POINT = 2;
+       /**
+        * ligature caret subtable type
+        */
+       public static final int GDEF_LOOKUP_TYPE_LIGATURE_CARET = 3;
+       /**
+        * mark attachment subtable type
+        */
+       public static final int GDEF_LOOKUP_TYPE_MARK_ATTACHMENT = 4;
+
+       /**
+        * pre-defined glyph class - base glyph
+        */
+       public static final int GLYPH_CLASS_BASE = 1;
+       /**
+        * pre-defined glyph class - ligature glyph
+        */
+       public static final int GLYPH_CLASS_LIGATURE = 2;
+       /**
+        * pre-defined glyph class - mark glyph
+        */
+       public static final int GLYPH_CLASS_MARK = 3;
+       /**
+        * pre-defined glyph class - component glyph
+        */
+       public static final int GLYPH_CLASS_COMPONENT = 4;
+
+       /**
+        * singleton glyph class table
+        */
+       private GlyphClassSubtable gct;
+       /** singleton attachment point table */
+       // private AttachmentPointSubtable apt; // NOT YET USED
+       /** singleton ligature caret table */
+       // private LigatureCaretSubtable lct;   // NOT YET USED
+       /**
+        * singleton mark attachment table
+        */
+       private MarkAttachmentSubtable mat;
+
+       /**
+        * Instantiate a <code>GlyphDefinitionTable</code> object using the specified subtables.
+        *
+        * @param subtables a list of identified subtables
+        */
+       public GlyphDefinitionTable(List subtables) {
+               super(null, new HashMap(0));
+               if ((subtables == null) || (subtables.size() == 0)) {
+                       throw new AdvancedTypographicTableFormatException("subtables must be non-empty");
+               } else {
+                       for (Iterator it = subtables.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (o instanceof GlyphDefinitionSubtable) {
+                                       addSubtable((GlyphSubtable) o);
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException("subtable must be a glyph definition subtable");
+                               }
+                       }
+                       freezeSubtables();
+               }
+       }
+
+       /**
+        * Reorder combining marks in glyph sequence so that they precede (within the sequence) the base
+        * character to which they are applied. N.B. In the case of LTR segments, marks are not reordered by this,
+        * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede
+        * their base glyph.
+        *
+        * @param gs       an input glyph sequence
+        * @param widths   associated advance widths (also reordered)
+        * @param gpa      associated glyph position adjustments (also reordered)
+        * @param script   a script identifier
+        * @param language a language identifier
+        * @return the reordered (output) glyph sequence
+        */
+       public GlyphSequence reorderCombiningMarks(GlyphSequence gs, int[] widths, int[][] gpa, String script,
+                                                                                          String language) {
+               ScriptProcessor sp = ScriptProcessor.getInstance(script);
+               return sp.reorderCombiningMarks(this, gs, widths, gpa, script, language);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       protected void addSubtable(GlyphSubtable subtable) {
+               if (subtable instanceof GlyphClassSubtable) {
+                       this.gct = (GlyphClassSubtable) subtable;
+               } else if (subtable instanceof AttachmentPointSubtable) {
+                       // TODO - not yet used
+                       // this.apt = (AttachmentPointSubtable) subtable;
+               } else if (subtable instanceof LigatureCaretSubtable) {
+                       // TODO - not yet used
+                       // this.lct = (LigatureCaretSubtable) subtable;
+               } else if (subtable instanceof MarkAttachmentSubtable) {
+                       this.mat = (MarkAttachmentSubtable) subtable;
+               } else {
+                       throw new UnsupportedOperationException("unsupported glyph definition subtable type: " + subtable);
+               }
+       }
+
+       /**
+        * Determine if glyph belongs to pre-defined glyph class.
+        *
+        * @param gid a glyph identifier (index)
+        * @param gc  a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT).
+        * @return true if glyph belongs to specified glyph class
+        */
+       public boolean isGlyphClass(int gid, int gc) {
+               if (gct != null) {
+                       return gct.isGlyphClass(gid, gc);
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Determine glyph class.
+        *
+        * @param gid a glyph identifier (index)
+        * @return a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT).
+        */
+       public int getGlyphClass(int gid) {
+               if (gct != null) {
+                       return gct.getGlyphClass(gid);
+               } else {
+                       return -1;
+               }
+       }
+
+       /**
+        * Determine if glyph belongs to (font specific) mark attachment class.
+        *
+        * @param gid a glyph identifier (index)
+        * @param mac a (font specific) mark attachment class
+        * @return true if glyph belongs to specified mark attachment class
+        */
+       public boolean isMarkAttachClass(int gid, int mac) {
+               if (mat != null) {
+                       return mat.isMarkAttachClass(gid, mac);
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Determine mark attachment class.
+        *
+        * @param gid a glyph identifier (index)
+        * @return a non-negative mark attachment class, or -1 if no class defined
+        */
+       public int getMarkAttachClass(int gid) {
+               if (mat != null) {
+                       return mat.getMarkAttachClass(gid);
+               } else {
+                       return -1;
+               }
+       }
+
+       /**
+        * Map a lookup type name to its constant (integer) value.
+        *
+        * @param name lookup type name
+        * @return lookup type
+        */
+       public static int getLookupTypeFromName(String name) {
+               int t;
+               String s = name.toLowerCase();
+               if ("glyphclass".equals(s)) {
+                       t = GDEF_LOOKUP_TYPE_GLYPH_CLASS;
+               } else if ("attachmentpoint".equals(s)) {
+                       t = GDEF_LOOKUP_TYPE_ATTACHMENT_POINT;
+               } else if ("ligaturecaret".equals(s)) {
+                       t = GDEF_LOOKUP_TYPE_LIGATURE_CARET;
+               } else if ("markattachment".equals(s)) {
+                       t = GDEF_LOOKUP_TYPE_MARK_ATTACHMENT;
+               } else {
+                       t = -1;
+               }
+               return t;
+       }
+
+       /**
+        * Map a lookup type constant (integer) value to its name.
+        *
+        * @param type lookup type
+        * @return lookup type name
+        */
+       public static String getLookupTypeName(int type) {
+               String tn = null;
+               switch (type) {
+                       case GDEF_LOOKUP_TYPE_GLYPH_CLASS:
+                               tn = "glyphclass";
+                               break;
+                       case GDEF_LOOKUP_TYPE_ATTACHMENT_POINT:
+                               tn = "attachmentpoint";
+                               break;
+                       case GDEF_LOOKUP_TYPE_LIGATURE_CARET:
+                               tn = "ligaturecaret";
+                               break;
+                       case GDEF_LOOKUP_TYPE_MARK_ATTACHMENT:
+                               tn = "markattachment";
+                               break;
+                       default:
+                               tn = "unknown";
+                               break;
+               }
+               return tn;
+       }
+
+       /**
+        * Create a definition subtable according to the specified arguments.
+        *
+        * @param type     subtable type
+        * @param id       subtable identifier
+        * @param sequence subtable sequence
+        * @param flags    subtable flags (must be zero)
+        * @param format   subtable format
+        * @param mapping  subtable mapping table
+        * @param entries  subtable entries
+        * @return a glyph subtable instance
+        */
+       public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format,
+                                                                                          GlyphMappingTable mapping, List entries) {
+               GlyphSubtable st = null;
+               switch (type) {
+                       case GDEF_LOOKUP_TYPE_GLYPH_CLASS:
+                               st = GlyphClassSubtable.create(id, sequence, flags, format, mapping, entries);
+                               break;
+                       case GDEF_LOOKUP_TYPE_ATTACHMENT_POINT:
+                               st = AttachmentPointSubtable.create(id, sequence, flags, format, mapping, entries);
+                               break;
+                       case GDEF_LOOKUP_TYPE_LIGATURE_CARET:
+                               st = LigatureCaretSubtable.create(id, sequence, flags, format, mapping, entries);
+                               break;
+                       case GDEF_LOOKUP_TYPE_MARK_ATTACHMENT:
+                               st = MarkAttachmentSubtable.create(id, sequence, flags, format, mapping, entries);
+                               break;
+                       default:
+                               break;
+               }
+               return st;
+       }
+
+       private abstract static class GlyphClassSubtable extends GlyphDefinitionSubtable {
+
+               GlyphClassSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) {
+                       super(id, sequence, flags, format, mapping);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GDEF_LOOKUP_TYPE_GLYPH_CLASS;
+               }
+
+               /**
+                * Determine if glyph belongs to pre-defined glyph class.
+                *
+                * @param gid a glyph identifier (index)
+                * @param gc  a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT).
+                * @return true if glyph belongs to specified glyph class
+                */
+               public abstract boolean isGlyphClass(int gid, int gc);
+
+               /**
+                * Determine glyph class.
+                *
+                * @param gid a glyph identifier (index)
+                * @return a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT).
+                */
+               public abstract int getGlyphClass(int gid);
+
+               static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping,
+                                                                                         List entries) {
+                       if (format == 1) {
+                               return new GlyphClassSubtableFormat1(id, sequence, flags, format, mapping, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class GlyphClassSubtableFormat1 extends GlyphClassSubtable {
+
+               GlyphClassSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) {
+                       super(id, sequence, flags, format, mapping, entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       return null;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof GlyphClassSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isGlyphClass(int gid, int gc) {
+                       GlyphClassMapping cm = getClasses();
+                       if (cm != null) {
+                               return cm.getClassIndex(gid, 0) == gc;
+                       } else {
+                               return false;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getGlyphClass(int gid) {
+                       GlyphClassMapping cm = getClasses();
+                       if (cm != null) {
+                               return cm.getClassIndex(gid, 0);
+                       } else {
+                               return -1;
+                       }
+               }
+       }
+
+       private abstract static class AttachmentPointSubtable extends GlyphDefinitionSubtable {
+
+               AttachmentPointSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) {
+                       super(id, sequence, flags, format, mapping);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GDEF_LOOKUP_TYPE_ATTACHMENT_POINT;
+               }
+
+               static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping,
+                                                                                         List entries) {
+                       if (format == 1) {
+                               return new AttachmentPointSubtableFormat1(id, sequence, flags, format, mapping, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class AttachmentPointSubtableFormat1 extends AttachmentPointSubtable {
+
+               AttachmentPointSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping,
+                                                                          List entries) {
+                       super(id, sequence, flags, format, mapping, entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       return null;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof AttachmentPointSubtable;
+               }
+       }
+
+       private abstract static class LigatureCaretSubtable extends GlyphDefinitionSubtable {
+
+               LigatureCaretSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) {
+                       super(id, sequence, flags, format, mapping);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GDEF_LOOKUP_TYPE_LIGATURE_CARET;
+               }
+
+               static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping,
+                                                                                         List entries) {
+                       if (format == 1) {
+                               return new LigatureCaretSubtableFormat1(id, sequence, flags, format, mapping, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class LigatureCaretSubtableFormat1 extends LigatureCaretSubtable {
+
+               LigatureCaretSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping,
+                                                                        List entries) {
+                       super(id, sequence, flags, format, mapping, entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       return null;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof LigatureCaretSubtable;
+               }
+       }
+
+       private abstract static class MarkAttachmentSubtable extends GlyphDefinitionSubtable {
+
+               MarkAttachmentSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) {
+                       super(id, sequence, flags, format, mapping);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GDEF_LOOKUP_TYPE_MARK_ATTACHMENT;
+               }
+
+               /**
+                * Determine if glyph belongs to (font specific) mark attachment class.
+                *
+                * @param gid a glyph identifier (index)
+                * @param mac a (font specific) mark attachment class
+                * @return true if glyph belongs to specified mark attachment class
+                */
+               public abstract boolean isMarkAttachClass(int gid, int mac);
+
+               /**
+                * Determine mark attachment class.
+                *
+                * @param gid a glyph identifier (index)
+                * @return a non-negative mark attachment class, or -1 if no class defined
+                */
+               public abstract int getMarkAttachClass(int gid);
+
+               static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping,
+                                                                                         List entries) {
+                       if (format == 1) {
+                               return new MarkAttachmentSubtableFormat1(id, sequence, flags, format, mapping, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class MarkAttachmentSubtableFormat1 extends MarkAttachmentSubtable {
+
+               MarkAttachmentSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping,
+                                                                         List entries) {
+                       super(id, sequence, flags, format, mapping, entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       return null;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof MarkAttachmentSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isMarkAttachClass(int gid, int mac) {
+                       GlyphClassMapping cm = getClasses();
+                       if (cm != null) {
+                               return cm.getClassIndex(gid, 0) == mac;
+                       } else {
+                               return false;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMarkAttachClass(int gid) {
+                       GlyphClassMapping cm = getClasses();
+                       if (cm != null) {
+                               return cm.getClassIndex(gid, 0);
+                       } else {
+                               return -1;
+                       }
+               }
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphMappingTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphMappingTable.java
new file mode 100644 (file)
index 0000000..8bb9d6c
--- /dev/null
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * <p>Base class implementation of glyph mapping table. This base
+ * class maps glyph indices to arbitrary integers (mappping indices), and
+ * is used to implement both glyph coverage and glyph class maps.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GlyphMappingTable {
+
+       /**
+        * empty mapping table
+        */
+       public static final int GLYPH_MAPPING_TYPE_EMPTY = 0;
+
+       /**
+        * mapped mapping table
+        */
+       public static final int GLYPH_MAPPING_TYPE_MAPPED = 1;
+
+       /**
+        * range based mapping table
+        */
+       public static final int GLYPH_MAPPING_TYPE_RANGE = 2;
+
+       /**
+        * Obtain mapping type.
+        *
+        * @return mapping format type
+        */
+       public int getType() {
+               return -1;
+       }
+
+       /**
+        * Obtain mapping entries.
+        *
+        * @return list of mapping entries
+        */
+       public List getEntries() {
+               return null;
+       }
+
+       /**
+        * Obtain size of mapping table, i.e., ciMax + 1, where ciMax is the maximum
+        * mapping index.
+        *
+        * @return size of mapping table
+        */
+       public int getMappingSize() {
+               return 0;
+       }
+
+       /**
+        * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of
+        * the mapping table.
+        *
+        * @param gid glyph identifier (code)
+        * @return non-negative glyph mapping index or -1 if glyph identifiers is not mapped by table
+        */
+       public int getMappedIndex(int gid) {
+               return -1;
+       }
+
+       /**
+        * empty mapping table base class
+        */
+       protected static class EmptyMappingTable extends GlyphMappingTable {
+
+               /**
+                * Construct empty mapping table.
+                */
+               public EmptyMappingTable() {
+                       this(null);
+               }
+
+               /**
+                * Construct empty mapping table with entries (ignored).
+                *
+                * @param entries list of entries (ignored)
+                */
+               public EmptyMappingTable(List entries) {
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GLYPH_MAPPING_TYPE_EMPTY;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       return new java.util.ArrayList();
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMappingSize() {
+                       return 0;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMappedIndex(int gid) {
+                       return -1;
+               }
+       }
+
+       /**
+        * mapped mapping table base class
+        */
+       protected static class MappedMappingTable extends GlyphMappingTable {
+
+               /**
+                * Construct mapped mapping table.
+                */
+               public MappedMappingTable() {
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GLYPH_MAPPING_TYPE_MAPPED;
+               }
+       }
+
+       /**
+        * range mapping table base class
+        */
+       protected abstract static class RangeMappingTable extends GlyphMappingTable {
+
+               private int[] sa;                                                // array of range (inclusive) starts
+               private int[] ea;                                                // array of range (inclusive) ends
+               private int[] ma;                                                // array of range mapped values
+               private int miMax = -1;
+
+               /**
+                * Construct range mapping table.
+                *
+                * @param entries of mapping ranges
+                */
+               public RangeMappingTable(List entries) {
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GLYPH_MAPPING_TYPE_RANGE;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       List entries = new java.util.ArrayList();
+                       if (sa != null) {
+                               for (int i = 0, n = sa.length; i < n; i++) {
+                                       entries.add(new MappingRange(sa[i], ea[i], ma[i]));
+                               }
+                       }
+                       return entries;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMappingSize() {
+                       return miMax + 1;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMappedIndex(int gid) {
+                       int i;
+                       int mi;
+                       if ((i = Arrays.binarySearch(sa, gid)) >= 0) {
+                               mi = getMappedIndex(gid, sa[i], ma[i]);                // matches start of (some) range
+                       } else if ((i = -(i + 1)) == 0) {
+                               mi = -1;                                                        // precedes first range
+                       } else if (gid > ea[--i]) {
+                               mi = -1;                                                        // follows preceding (or last) range
+                       } else {
+                               mi = getMappedIndex(gid, sa[i], ma[i]);                // intersects (some) range
+                       }
+                       return mi;
+               }
+
+               /**
+                * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of
+                * the mapping table.
+                *
+                * @param gid glyph identifier (code)
+                * @param s   start of range
+                * @param m   mapping value
+                * @return non-negative glyph mapping index or -1 if glyph identifiers is not mapped by table
+                */
+               public abstract int getMappedIndex(int gid, int s, int m);
+
+               private void populate(List entries) {
+                       int i = 0;
+                       int n = entries.size();
+                       int gidMax = -1;
+                       int miMax = -1;
+                       int[] sa = new int[n];
+                       int[] ea = new int[n];
+                       int[] ma = new int[n];
+                       for (Iterator it = entries.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (o instanceof MappingRange) {
+                                       MappingRange r = (MappingRange) o;
+                                       int gs = r.getStart();
+                                       int ge = r.getEnd();
+                                       int mi = r.getIndex();
+                                       if ((gs < 0) || (gs > 65535)) {
+                                               throw new AdvancedTypographicTableFormatException(
+                                                               "illegal glyph range: [" + gs + "," + ge + "]: bad start index");
+                                       } else if ((ge < 0) || (ge > 65535)) {
+                                               throw new AdvancedTypographicTableFormatException(
+                                                               "illegal glyph range: [" + gs + "," + ge + "]: bad end index");
+                                       } else if (gs > ge) {
+                                               throw new AdvancedTypographicTableFormatException(
+                                                               "illegal glyph range: [" + gs + "," + ge + "]: start index exceeds end index");
+                                       } else if (gs < gidMax) {
+                                               throw new AdvancedTypographicTableFormatException("out of order glyph range: [" + gs + "," + ge + "]");
+                                       } else if (mi < 0) {
+                                               throw new AdvancedTypographicTableFormatException("illegal mapping index: " + mi);
+                                       } else {
+                                               int miLast;
+                                               sa[i] = gs;
+                                               ea[i] = gidMax = ge;
+                                               ma[i] = mi;
+                                               if ((miLast = mi + (ge - gs)) > miMax) {
+                                                       miMax = miLast;
+                                               }
+                                               i++;
+                                       }
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException("illegal mapping entry, must be Integer: " + o);
+                               }
+                       }
+                       assert i == n;
+                       assert this.sa == null;
+                       assert this.ea == null;
+                       assert this.ma == null;
+                       this.sa = sa;
+                       this.ea = ea;
+                       this.ma = ma;
+                       this.miMax = miMax;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append('{');
+                       for (int i = 0, n = sa.length; i < n; i++) {
+                               if (i > 0) {
+                                       sb.append(',');
+                               }
+                               sb.append('[');
+                               sb.append(Integer.toString(sa[i]));
+                               sb.append(Integer.toString(ea[i]));
+                               sb.append("]:");
+                               sb.append(Integer.toString(ma[i]));
+                       }
+                       sb.append('}');
+                       return sb.toString();
+               }
+       }
+
+       /**
+        * The <code>MappingRange</code> class encapsulates a glyph [start,end] range and
+        * a mapping index.
+        */
+       public static class MappingRange {
+
+               private final int gidStart;                     // first glyph in range (inclusive)
+               private final int gidEnd;                       // last glyph in range (inclusive)
+               private final int index;                        // mapping index;
+
+               /**
+                * Instantiate a mapping range.
+                */
+               public MappingRange() {
+                       this(0, 0, 0);
+               }
+
+               /**
+                * Instantiate a specific mapping range.
+                *
+                * @param gidStart start of range
+                * @param gidEnd   end of range
+                * @param index    mapping index
+                */
+               public MappingRange(int gidStart, int gidEnd, int index) {
+                       if ((gidStart < 0) || (gidEnd < 0) || (index < 0)) {
+                               throw new AdvancedTypographicTableFormatException();
+                       } else if (gidStart > gidEnd) {
+                               throw new AdvancedTypographicTableFormatException();
+                       } else {
+                               this.gidStart = gidStart;
+                               this.gidEnd = gidEnd;
+                               this.index = index;
+                       }
+               }
+
+               /**
+                * @return start of range
+                */
+               public int getStart() {
+                       return gidStart;
+               }
+
+               /**
+                * @return end of range
+                */
+               public int getEnd() {
+                       return gidEnd;
+               }
+
+               /**
+                * @return mapping index
+                */
+               public int getIndex() {
+                       return index;
+               }
+
+               /**
+                * @return interval as a pair of integers
+                */
+               public int[] getInterval() {
+                       return new int[]{gidStart, gidEnd};
+               }
+
+               /**
+                * Obtain interval, filled into first two elements of specified array, or returning new array.
+                *
+                * @param interval an array of length two or greater or null
+                * @return interval as a pair of integers, filled into specified array
+                */
+               public int[] getInterval(int[] interval) {
+                       if ((interval == null) || (interval.length != 2)) {
+                               throw new IllegalArgumentException();
+                       } else {
+                               interval[0] = gidStart;
+                               interval[1] = gidEnd;
+                       }
+                       return interval;
+               }
+
+               /**
+                * @return length of interval
+                */
+               public int getLength() {
+                       return gidStart - gidEnd;
+               }
+
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioning.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioning.java
new file mode 100644 (file)
index 0000000..feaab2f
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+/**
+ * <p>The <code>GlyphPositioning</code> interface is implemented by a glyph positioning subtable
+ * that supports the determination of glyph positioning information based on script and
+ * language of the corresponding character content.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface GlyphPositioning {
+
+       /**
+        * Perform glyph positioning at the current index, mutating the positioning state object as required.
+        * Only the context associated with the current index is processed.
+        *
+        * @param ps glyph positioning state object
+        * @return true if the glyph subtable applies, meaning that the current context matches the
+        * associated input context glyph coverage table; note that returning true does not mean any position
+        * adjustment occurred; it only means that no further glyph subtables for the current lookup table
+        * should be applied.
+        */
+       boolean position(GlyphPositioningState ps);
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningState.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningState.java
new file mode 100644 (file)
index 0000000..0bdb7f9
--- /dev/null
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.ScriptContextTester;
+
+/**
+ * <p>The <code>GlyphPositioningState</code> implements an state object used during glyph positioning
+ * processing.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GlyphPositioningState extends GlyphProcessingState {
+
+       /**
+        * font size
+        */
+       private int fontSize;
+       /**
+        * default advancements
+        */
+       private int[] widths;
+       /**
+        * current adjustments
+        */
+       private int[][] adjustments;
+       /**
+        * if true, then some adjustment was applied
+        */
+       private boolean adjusted;
+
+       /**
+        * Construct default (reset) glyph positioning state.
+        */
+       public GlyphPositioningState() {
+       }
+
+       /**
+        * Construct glyph positioning state.
+        *
+        * @param gs          input glyph sequence
+        * @param script      script identifier
+        * @param language    language identifier
+        * @param feature     feature identifier
+        * @param fontSize    font size (in micropoints)
+        * @param widths      array of design advancements (in glyph index order)
+        * @param adjustments positioning adjustments to which positioning is applied
+        * @param sct         script context tester (or null)
+        */
+       public GlyphPositioningState(GlyphSequence gs, String script, String language, String feature, int fontSize,
+                                                                int[] widths, int[][] adjustments, ScriptContextTester sct) {
+               super(gs, script, language, feature, sct);
+               this.fontSize = fontSize;
+               this.widths = widths;
+               this.adjustments = adjustments;
+       }
+
+       /**
+        * Construct glyph positioning state using an existing state object using shallow copy
+        * except as follows: input glyph sequence is copied deep except for its characters array.
+        *
+        * @param ps existing positioning state to copy from
+        */
+       public GlyphPositioningState(GlyphPositioningState ps) {
+               super(ps);
+               this.fontSize = ps.fontSize;
+               this.widths = ps.widths;
+               this.adjustments = ps.adjustments;
+       }
+
+       /**
+        * Reset glyph positioning state.
+        *
+        * @param gs          input glyph sequence
+        * @param script      script identifier
+        * @param language    language identifier
+        * @param feature     feature identifier
+        * @param fontSize    font size (in micropoints)
+        * @param widths      array of design advancements (in glyph index order)
+        * @param adjustments positioning adjustments to which positioning is applied
+        * @param sct         script context tester (or null)
+        */
+       public GlyphPositioningState reset(GlyphSequence gs, String script, String language, String feature, int fontSize,
+                                                                          int[] widths, int[][] adjustments, ScriptContextTester sct) {
+               super.reset(gs, script, language, feature, sct);
+               this.fontSize = fontSize;
+               this.widths = widths;
+               this.adjustments = adjustments;
+               this.adjusted = false;
+               return this;
+       }
+
+       /**
+        * Obtain design advancement (width) of glyph at specified index.
+        *
+        * @param gi glyph index
+        * @return design advancement, or zero if glyph index is not present
+        */
+       public int getWidth(int gi) {
+               if ((widths != null) && (gi < widths.length)) {
+                       return widths[gi];
+               } else {
+                       return 0;
+               }
+       }
+
+       /**
+        * Perform adjustments at current position index.
+        *
+        * @param v value containing adjustments
+        * @return true if a non-zero adjustment was made
+        */
+       public boolean adjust(GlyphPositioningTable.Value v) {
+               return adjust(v, 0);
+       }
+
+       /**
+        * Perform adjustments at specified offset from current position index.
+        *
+        * @param v      value containing adjustments
+        * @param offset from current position index
+        * @return true if a non-zero adjustment was made
+        */
+       public boolean adjust(GlyphPositioningTable.Value v, int offset) {
+               assert v != null;
+               if ((index + offset) < indexLast) {
+                       return v.adjust(adjustments[index + offset], fontSize);
+               } else {
+                       throw new IndexOutOfBoundsException();
+               }
+       }
+
+       /**
+        * Obtain current adjustments at current position index.
+        *
+        * @return array of adjustments (int[4]) at current position
+        */
+       public int[] getAdjustment() {
+               return getAdjustment(0);
+       }
+
+       /**
+        * Obtain current adjustments at specified offset from current position index.
+        *
+        * @param offset from current position index
+        * @return array of adjustments (int[4]) at specified offset
+        * @throws IndexOutOfBoundsException if offset is invalid
+        */
+       public int[] getAdjustment(int offset) throws IndexOutOfBoundsException {
+               if ((index + offset) < indexLast) {
+                       return adjustments[index + offset];
+               } else {
+                       throw new IndexOutOfBoundsException();
+               }
+       }
+
+       /**
+        * Apply positioning subtable to current state at current position (only),
+        * resulting in the consumption of zero or more input glyphs.
+        *
+        * @param st the glyph positioning subtable to apply
+        * @return true if subtable applied, or false if it did not (e.g., its
+        * input coverage table did not match current input context)
+        */
+       public boolean apply(GlyphPositioningSubtable st) {
+               assert st != null;
+               updateSubtableState(st);
+               boolean applied = st.position(this);
+               return applied;
+       }
+
+       /**
+        * Apply a sequence of matched rule lookups to the <code>nig</code> input glyphs
+        * starting at the current position. If lookups are non-null and non-empty, then
+        * all input glyphs specified by <code>nig</code> are consumed irregardless of
+        * whether any specified lookup applied.
+        *
+        * @param lookups array of matched lookups (or null)
+        * @param nig     number of glyphs in input sequence, starting at current position, to which
+        *                the lookups are to apply, and to be consumed once the application has finished
+        * @return true if lookups are non-null and non-empty; otherwise, false
+        */
+       public boolean apply(GlyphTable.RuleLookup[] lookups, int nig) {
+               if ((lookups != null) && (lookups.length > 0)) {
+                       // apply each rule lookup to extracted input glyph array
+                       for (int i = 0, n = lookups.length; i < n; i++) {
+                               GlyphTable.RuleLookup l = lookups[i];
+                               if (l != null) {
+                                       GlyphTable.LookupTable lt = l.getLookup();
+                                       if (lt != null) {
+                                               // perform positioning on a copy of previous state
+                                               GlyphPositioningState ps = new GlyphPositioningState(this);
+                                               // apply lookup table positioning
+                                               if (lt.position(ps, l.getSequenceIndex())) {
+                                                       setAdjusted(true);
+                                               }
+                                       }
+                               }
+                       }
+                       consume(nig);
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Apply default application semantices; namely, consume one input glyph.
+        */
+       public void applyDefault() {
+               super.applyDefault();
+       }
+
+       /**
+        * Set adjusted state, used to record effect of non-zero adjustment.
+        *
+        * @param adjusted true if to set adjusted state, otherwise false to
+        *                 clear adjusted state
+        */
+       public void setAdjusted(boolean adjusted) {
+               this.adjusted = adjusted;
+       }
+
+       /**
+        * Get adjusted state.
+        *
+        * @return adjusted true if some non-zero adjustment occurred and
+        * was recorded by {@link #setAdjusted}; otherwise, false.
+        */
+       public boolean getAdjusted() {
+               return adjusted;
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningSubtable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningSubtable.java
new file mode 100644 (file)
index 0000000..8afe96b
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import com.jaredrummler.fontreader.fonts.GlyphSubtable;
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.ScriptContextTester;
+
+/**
+ * <p>The <code>GlyphPositioningSubtable</code> implements an abstract base of a glyph subtable,
+ * providing a default implementation of the <code>GlyphPositioning</code> interface.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public abstract class GlyphPositioningSubtable extends GlyphSubtable implements GlyphPositioning {
+
+       /**
+        * Instantiate a <code>GlyphPositioningSubtable</code>.
+        *
+        * @param id       subtable identifier
+        * @param sequence subtable sequence
+        * @param flags    subtable flags
+        * @param format   subtable format
+        * @param coverage subtable coverage table
+        */
+       protected GlyphPositioningSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage) {
+               super(id, sequence, flags, format, coverage);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int getTableType() {
+               return GlyphTable.GLYPH_TABLE_TYPE_POSITIONING;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String getTypeName() {
+               return GlyphPositioningTable.getLookupTypeName(getType());
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean isCompatible(GlyphSubtable subtable) {
+               return subtable instanceof GlyphPositioningSubtable;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean usesReverseScan() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean position(GlyphPositioningState ps) {
+               return false;
+       }
+
+       /**
+        * Apply positioning using specified state and subtable array. For each position in input sequence,
+        * apply subtables in order until some subtable applies or none remain. If no subtable applied or no
+        * input was consumed for a given position, then apply default action (no adjustments and advance).
+        * If <code>sequenceIndex</code> is non-negative, then apply subtables only when current position
+        * matches <code>sequenceIndex</code> in relation to the starting position. Furthermore, upon
+        * successful application at <code>sequenceIndex</code>, then discontinue processing the remaining
+        *
+        * @param ps            positioning state
+        * @param sta           array of subtables to apply
+        * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
+        * @return true if a non-zero adjustment occurred
+        */
+       public static boolean position(GlyphPositioningState ps, GlyphPositioningSubtable[] sta, int sequenceIndex) {
+               int sequenceStart = ps.getPosition();
+               boolean appliedOneShot = false;
+               while (ps.hasNext()) {
+                       boolean applied = false;
+                       if (!appliedOneShot && ps.maybeApplicable()) {
+                               for (int i = 0, n = sta.length; !applied && (i < n); i++) {
+                                       if (sequenceIndex < 0) {
+                                               applied = ps.apply(sta[i]);
+                                       } else if (ps.getPosition() == (sequenceStart + sequenceIndex)) {
+                                               applied = ps.apply(sta[i]);
+                                               if (applied) {
+                                                       appliedOneShot = true;
+                                               }
+                                       }
+                               }
+                       }
+                       if (!applied || !ps.didConsume()) {
+                               ps.applyDefault();
+                       }
+                       ps.next();
+               }
+               return ps.getAdjusted();
+       }
+
+       /**
+        * Apply positioning.
+        *
+        * @param gs          input glyph sequence
+        * @param script      tag
+        * @param language    tag
+        * @param feature     tag
+        * @param fontSize    the font size
+        * @param sta         subtable array
+        * @param widths      array
+        * @param adjustments array (receives output adjustments)
+        * @param sct         script context tester
+        * @return true if a non-zero adjustment occurred
+        */
+       public static boolean position(GlyphSequence gs, String script, String language, String feature, int fontSize,
+                                                                  GlyphPositioningSubtable[] sta, int[] widths, int[][] adjustments,
+                                                                  ScriptContextTester sct) {
+               GlyphPositioningState state = new GlyphPositioningState();
+               return position(state.reset(gs, script, language, feature, fontSize, widths, adjustments, sct), sta, -1);
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningTable.java
new file mode 100644 (file)
index 0000000..073fe30
--- /dev/null
@@ -0,0 +1,2754 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import com.jaredrummler.fontreader.complexscripts.scripts.ScriptProcessor;
+import com.jaredrummler.fontreader.fonts.GlyphSubtable;
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.GlyphTester;
+
+import java.util.*;
+
+/**
+ * <p>The <code>GlyphPositioningTable</code> class is a glyph table that implements
+ * <code>GlyphPositioning</code> functionality.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GlyphPositioningTable extends GlyphTable {
+
+       /**
+        * single positioning subtable type
+        */
+       public static final int GPOS_LOOKUP_TYPE_SINGLE = 1;
+       /**
+        * multiple positioning subtable type
+        */
+       public static final int GPOS_LOOKUP_TYPE_PAIR = 2;
+       /**
+        * cursive positioning subtable type
+        */
+       public static final int GPOS_LOOKUP_TYPE_CURSIVE = 3;
+       /**
+        * mark to base positioning subtable type
+        */
+       public static final int GPOS_LOOKUP_TYPE_MARK_TO_BASE = 4;
+       /**
+        * mark to ligature positioning subtable type
+        */
+       public static final int GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE = 5;
+       /**
+        * mark to mark positioning subtable type
+        */
+       public static final int GPOS_LOOKUP_TYPE_MARK_TO_MARK = 6;
+       /**
+        * contextual positioning subtable type
+        */
+       public static final int GPOS_LOOKUP_TYPE_CONTEXTUAL = 7;
+       /**
+        * chained contextual positioning subtable type
+        */
+       public static final int GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL = 8;
+       /**
+        * extension positioning subtable type
+        */
+       public static final int GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING = 9;
+
+       /**
+        * Instantiate a <code>GlyphPositioningTable</code> object using the specified lookups
+        * and subtables.
+        *
+        * @param gdef      glyph definition table that applies
+        * @param lookups   a map of lookup specifications to subtable identifier strings
+        * @param subtables a list of identified subtables
+        */
+       public GlyphPositioningTable(GlyphDefinitionTable gdef, Map lookups, List subtables) {
+               super(gdef, lookups);
+               if ((subtables == null) || (subtables.size() == 0)) {
+                       throw new AdvancedTypographicTableFormatException("subtables must be non-empty");
+               } else {
+                       for (Iterator it = subtables.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (o instanceof GlyphPositioningSubtable) {
+                                       addSubtable((GlyphSubtable) o);
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException("subtable must be a glyph positioning subtable");
+                               }
+                       }
+                       freezeSubtables();
+               }
+       }
+
+       /**
+        * Map a lookup type name to its constant (integer) value.
+        *
+        * @param name lookup type name
+        * @return lookup type
+        */
+       public static int getLookupTypeFromName(String name) {
+               int t;
+               String s = name.toLowerCase();
+               if ("single".equals(s)) {
+                       t = GPOS_LOOKUP_TYPE_SINGLE;
+               } else if ("pair".equals(s)) {
+                       t = GPOS_LOOKUP_TYPE_PAIR;
+               } else if ("cursive".equals(s)) {
+                       t = GPOS_LOOKUP_TYPE_CURSIVE;
+               } else if ("marktobase".equals(s)) {
+                       t = GPOS_LOOKUP_TYPE_MARK_TO_BASE;
+               } else if ("marktoligature".equals(s)) {
+                       t = GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE;
+               } else if ("marktomark".equals(s)) {
+                       t = GPOS_LOOKUP_TYPE_MARK_TO_MARK;
+               } else if ("contextual".equals(s)) {
+                       t = GPOS_LOOKUP_TYPE_CONTEXTUAL;
+               } else if ("chainedcontextual".equals(s)) {
+                       t = GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
+               } else if ("extensionpositioning".equals(s)) {
+                       t = GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING;
+               } else {
+                       t = -1;
+               }
+               return t;
+       }
+
+       /**
+        * Map a lookup type constant (integer) value to its name.
+        *
+        * @param type lookup type
+        * @return lookup type name
+        */
+       public static String getLookupTypeName(int type) {
+               String tn;
+               switch (type) {
+                       case GPOS_LOOKUP_TYPE_SINGLE:
+                               tn = "single";
+                               break;
+                       case GPOS_LOOKUP_TYPE_PAIR:
+                               tn = "pair";
+                               break;
+                       case GPOS_LOOKUP_TYPE_CURSIVE:
+                               tn = "cursive";
+                               break;
+                       case GPOS_LOOKUP_TYPE_MARK_TO_BASE:
+                               tn = "marktobase";
+                               break;
+                       case GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE:
+                               tn = "marktoligature";
+                               break;
+                       case GPOS_LOOKUP_TYPE_MARK_TO_MARK:
+                               tn = "marktomark";
+                               break;
+                       case GPOS_LOOKUP_TYPE_CONTEXTUAL:
+                               tn = "contextual";
+                               break;
+                       case GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL:
+                               tn = "chainedcontextual";
+                               break;
+                       case GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING:
+                               tn = "extensionpositioning";
+                               break;
+                       default:
+                               tn = "unknown";
+                               break;
+               }
+               return tn;
+       }
+
+       /**
+        * Create a positioning subtable according to the specified arguments.
+        *
+        * @param type     subtable type
+        * @param id       subtable identifier
+        * @param sequence subtable sequence
+        * @param flags    subtable flags
+        * @param format   subtable format
+        * @param coverage subtable coverage table
+        * @param entries  subtable entries
+        * @return a glyph subtable instance
+        */
+       public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format,
+                                                                                          GlyphCoverageTable coverage, List entries) {
+               GlyphSubtable st = null;
+               switch (type) {
+                       case GPOS_LOOKUP_TYPE_SINGLE:
+                               st = SingleSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GPOS_LOOKUP_TYPE_PAIR:
+                               st = PairSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GPOS_LOOKUP_TYPE_CURSIVE:
+                               st = CursiveSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GPOS_LOOKUP_TYPE_MARK_TO_BASE:
+                               st = MarkToBaseSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE:
+                               st = MarkToLigatureSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GPOS_LOOKUP_TYPE_MARK_TO_MARK:
+                               st = MarkToMarkSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GPOS_LOOKUP_TYPE_CONTEXTUAL:
+                               st = ContextualSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL:
+                               st = ChainedContextualSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       default:
+                               break;
+               }
+               return st;
+       }
+
+       /**
+        * Create a positioning subtable according to the specified arguments.
+        *
+        * @param type     subtable type
+        * @param id       subtable identifier
+        * @param sequence subtable sequence
+        * @param flags    subtable flags
+        * @param format   subtable format
+        * @param coverage list of coverage table entries
+        * @param entries  subtable entries
+        * @return a glyph subtable instance
+        */
+       public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format, List coverage,
+                                                                                          List entries) {
+               return createSubtable(type, id, sequence, flags, format, GlyphCoverageTable.createCoverageTable(coverage), entries);
+       }
+
+       /**
+        * Perform positioning processing using all matching lookups.
+        *
+        * @param gs          an input glyph sequence
+        * @param script      a script identifier
+        * @param language    a language identifier
+        * @param fontSize    size in device units
+        * @param widths      array of default advancements for each glyph
+        * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in
+        *                    that order,
+        *                    with one 4-tuple for each element of glyph sequence
+        * @return true if some adjustment is not zero; otherwise, false
+        */
+       public boolean position(GlyphSequence gs, String script, String language, int fontSize, int[] widths,
+                                                       int[][] adjustments) {
+               Map/*<LookupSpec,List<LookupTable>>*/ lookups = matchLookups(script, language, "*");
+               if ((lookups != null) && (lookups.size() > 0)) {
+                       ScriptProcessor sp = ScriptProcessor.getInstance(script);
+                       return sp.position(this, gs, script, language, fontSize, lookups, widths, adjustments);
+               } else {
+                       return false;
+               }
+       }
+
+       private abstract static class SingleSubtable extends GlyphPositioningSubtable {
+
+               SingleSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GPOS_LOOKUP_TYPE_SINGLE;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof SingleSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean position(GlyphPositioningState ps) {
+                       int gi = ps.getGlyph();
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) < 0) {
+                               return false;
+                       } else {
+                               Value v = getValue(ci, gi);
+                               if (v != null) {
+                                       if (ps.adjust(v)) {
+                                               ps.setAdjusted(true);
+                                       }
+                                       ps.consume(1);
+                               }
+                               return true;
+                       }
+               }
+
+               /**
+                * Obtain positioning value for coverage index.
+                *
+                * @param ci coverage index
+                * @param gi input glyph index
+                * @return positioning value or null if none applies
+                */
+               public abstract Value getValue(int ci, int gi);
+
+               static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                          List entries) {
+                       if (format == 1) {
+                               return new SingleSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 2) {
+                               return new SingleSubtableFormat2(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class SingleSubtableFormat1 extends SingleSubtable {
+
+               private Value value;
+               private int ciMax;
+
+               SingleSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (value != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(value);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public Value getValue(int ci, int gi) {
+                       if ((value != null) && (ci <= ciMax)) {
+                               return value;
+                       } else {
+                               return null;
+                       }
+               }
+
+               private void populate(List entries) {
+                       if ((entries == null) || (entries.size() != 1)) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, must be non-null and contain exactly one entry");
+                       } else {
+                               Value v;
+                               Object o = entries.get(0);
+                               if (o instanceof Value) {
+                                       v = (Value) o;
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries entry, must be Value, but is: " + ((o != null) ? o.getClass() : null));
+                               }
+                               assert this.value == null;
+                               this.value = v;
+                               this.ciMax = getCoverageSize() - 1;
+                       }
+               }
+       }
+
+       private static class SingleSubtableFormat2 extends SingleSubtable {
+
+               private Value[] values;
+
+               SingleSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (values != null) {
+                               List entries = new ArrayList(values.length);
+                               for (int i = 0, n = values.length; i < n; i++) {
+                                       entries.add(values[i]);
+                               }
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public Value getValue(int ci, int gi) {
+                       if ((values != null) && (ci < values.length)) {
+                               return values[ci];
+                       } else {
+                               return null;
+                       }
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof Value[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, single entry must be a Value[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       Value[] va = (Value[]) o;
+                                       if (va.length != getCoverageSize()) {
+                                               throw new AdvancedTypographicTableFormatException(
+                                                               "illegal values array, " + entries.size() + " values present, but requires " + getCoverageSize() +
+                                                                               " values");
+                                       } else {
+                                               assert this.values == null;
+                                               this.values = va;
+                                       }
+                               }
+                       }
+               }
+       }
+
+       private abstract static class PairSubtable extends GlyphPositioningSubtable {
+
+               PairSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GPOS_LOOKUP_TYPE_PAIR;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof PairSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean position(GlyphPositioningState ps) {
+                       boolean applied = false;
+                       int gi = ps.getGlyph(0);
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) >= 0) {
+                               int[] counts = ps.getGlyphsAvailable(0);
+                               int nga = counts[0];
+                               if (nga > 1) {
+                                       int[] iga = ps.getGlyphs(0, 2, null, counts);
+                                       if ((iga != null) && (iga.length == 2)) {
+                                               PairValues pv = getPairValues(ci, iga[0], iga[1]);
+                                               if (pv != null) {
+                                                       int offset = 0;
+                                                       int offsetLast = counts[0] + counts[1];
+                                                       // skip any ignored glyphs prior to first non-ignored glyph
+                                                       for (; offset < offsetLast; ++offset) {
+                                                               if (!ps.isIgnoredGlyph(offset)) {
+                                                                       break;
+                                                               } else {
+                                                                       ps.consume(1);
+                                                               }
+                                                       }
+                                                       // adjust first non-ignored glyph if first value isn't null
+                                                       Value v1 = pv.getValue1();
+                                                       if (v1 != null) {
+                                                               if (ps.adjust(v1, offset)) {
+                                                                       ps.setAdjusted(true);
+                                                               }
+                                                               ps.consume(1);          // consume first non-ignored glyph
+                                                               ++offset;
+                                                       }
+                                                       // skip any ignored glyphs prior to second non-ignored glyph
+                                                       for (; offset < offsetLast; ++offset) {
+                                                               if (!ps.isIgnoredGlyph(offset)) {
+                                                                       break;
+                                                               } else {
+                                                                       ps.consume(1);
+                                                               }
+                                                       }
+                                                       // adjust second non-ignored glyph if second value isn't null
+                                                       Value v2 = pv.getValue2();
+                                                       if (v2 != null) {
+                                                               if (ps.adjust(v2, offset)) {
+                                                                       ps.setAdjusted(true);
+                                                               }
+                                                               ps.consume(1);          // consume second non-ignored glyph
+                                                               ++offset;
+                                                       }
+                                                       applied = true;
+                                               }
+                                       }
+                               }
+                       }
+                       return applied;
+               }
+
+               /**
+                * Obtain associated pair values.
+                *
+                * @param ci  coverage index
+                * @param gi1 first input glyph index
+                * @param gi2 second input glyph index
+                * @return pair values or null if none applies
+                */
+               public abstract PairValues getPairValues(int ci, int gi1, int gi2);
+
+               static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                          List entries) {
+                       if (format == 1) {
+                               return new PairSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 2) {
+                               return new PairSubtableFormat2(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class PairSubtableFormat1 extends PairSubtable {
+
+               private PairValues[][] pvm;                     // pair values matrix
+
+               PairSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (pvm != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(pvm);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public PairValues getPairValues(int ci, int gi1, int gi2) {
+                       if ((pvm != null) && (ci < pvm.length)) {
+                               PairValues[] pvt = pvm[ci];
+                               for (int i = 0, n = pvt.length; i < n; i++) {
+                                       PairValues pv = pvt[i];
+                                       if (pv != null) {
+                                               int g = pv.getGlyph();
+                                               if (g < gi2) {
+                                                       continue;
+                                               } else if (g == gi2) {
+                                                       return pv;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof PairValues[][])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first (and only) entry must be a PairValues[][], but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else {
+                                       pvm = (PairValues[][]) o;
+                               }
+                       }
+               }
+       }
+
+       private static class PairSubtableFormat2 extends PairSubtable {
+
+               private GlyphClassTable cdt1;                   // class def table 1
+               private GlyphClassTable cdt2;                   // class def table 2
+               private int nc1;                                // class 1 count
+               private int nc2;                                // class 2 count
+               private PairValues[][] pvm;                     // pair values matrix
+
+               PairSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (pvm != null) {
+                               List entries = new ArrayList(5);
+                               entries.add(cdt1);
+                               entries.add(cdt2);
+                               entries.add(Integer.valueOf(nc1));
+                               entries.add(Integer.valueOf(nc2));
+                               entries.add(pvm);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public PairValues getPairValues(int ci, int gi1, int gi2) {
+                       if (pvm != null) {
+                               int c1 = cdt1.getClassIndex(gi1, 0);
+                               if ((c1 >= 0) && (c1 < nc1) && (c1 < pvm.length)) {
+                                       PairValues[] pvt = pvm[c1];
+                                       if (pvt != null) {
+                                               int c2 = cdt2.getClassIndex(gi2, 0);
+                                               if ((c2 >= 0) && (c2 < nc2) && (c2 < pvt.length)) {
+                                                       return pvt[c2];
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 5) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 5 entries");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof GlyphClassTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an GlyphClassTable, but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else {
+                                       cdt1 = (GlyphClassTable) o;
+                               }
+                               if (((o = entries.get(1)) == null) || !(o instanceof GlyphClassTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, second entry must be an GlyphClassTable, but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else {
+                                       cdt2 = (GlyphClassTable) o;
+                               }
+                               if (((o = entries.get(2)) == null) || !(o instanceof Integer)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, third entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       nc1 = ((Integer) (o)).intValue();
+                               }
+                               if (((o = entries.get(3)) == null) || !(o instanceof Integer)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, fourth entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       nc2 = ((Integer) (o)).intValue();
+                               }
+                               if (((o = entries.get(4)) == null) || !(o instanceof PairValues[][])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, fifth entry must be a PairValues[][], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       pvm = (PairValues[][]) o;
+                               }
+                       }
+               }
+       }
+
+       private abstract static class CursiveSubtable extends GlyphPositioningSubtable {
+
+               CursiveSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GPOS_LOOKUP_TYPE_CURSIVE;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof CursiveSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean position(GlyphPositioningState ps) {
+                       boolean applied = false;
+                       int gi = ps.getGlyph(0);
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) >= 0) {
+                               int[] counts = ps.getGlyphsAvailable(0);
+                               int nga = counts[0];
+                               if (nga > 1) {
+                                       int[] iga = ps.getGlyphs(0, 2, null, counts);
+                                       if ((iga != null) && (iga.length == 2)) {
+                                               // int gi1 = gi;
+                                               int ci1 = ci;
+                                               int gi2 = iga[1];
+                                               int ci2 = getCoverageIndex(gi2);
+                                               Anchor[] aa = getExitEntryAnchors(ci1, ci2);
+                                               if (aa != null) {
+                                                       Anchor exa = aa[0];
+                                                       Anchor ena = aa[1];
+                                                       // int exw = ps.getWidth ( gi1 );
+                                                       int enw = ps.getWidth(gi2);
+                                                       if ((exa != null) && (ena != null)) {
+                                                               Value v = ena.getAlignmentAdjustment(exa);
+                                                               v.adjust(-enw, 0, 0, 0);
+                                                               if (ps.adjust(v)) {
+                                                                       ps.setAdjusted(true);
+                                                               }
+                                                       }
+                                                       // consume only first glyph of exit/entry glyph pair
+                                                       ps.consume(1);
+                                                       applied = true;
+                                               }
+                                       }
+                               }
+                       }
+                       return applied;
+               }
+
+               /**
+                * Obtain exit anchor for first glyph with coverage index <code>ci1</code> and entry anchor for second
+                * glyph with coverage index <code>ci2</code>.
+                *
+                * @param ci1 coverage index of first glyph (may be negative)
+                * @param ci2 coverage index of second glyph (may be negative)
+                * @return array of two anchors or null if either coverage index is negative or corresponding anchor is
+                * missing, where the first entry is the exit anchor of the first glyph and the second entry is the
+                * entry anchor of the second glyph
+                */
+               public abstract Anchor[] getExitEntryAnchors(int ci1, int ci2);
+
+               static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                          List entries) {
+                       if (format == 1) {
+                               return new CursiveSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class CursiveSubtableFormat1 extends CursiveSubtable {
+
+               private Anchor[] aa;
+               // anchor array, where even entries are entry anchors, and odd entries are exit anchors
+
+               CursiveSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (aa != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(aa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public Anchor[] getExitEntryAnchors(int ci1, int ci2) {
+                       if ((ci1 >= 0) && (ci2 >= 0)) {
+                               int ai1 = (ci1 * 2) + 1; // ci1 denotes glyph with exit anchor
+                               int ai2 = (ci2 * 2) + 0; // ci2 denotes glyph with entry anchor
+                               if ((aa != null) && (ai1 < aa.length) && (ai2 < aa.length)) {
+                                       Anchor exa = aa[ai1];
+                                       Anchor ena = aa[ai2];
+                                       if ((exa != null) && (ena != null)) {
+                                               return new Anchor[]{exa, ena};
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof Anchor[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first (and only) entry must be a Anchor[], but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else if ((((Anchor[]) o).length % 2) != 0) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, Anchor[] array must have an even number of entries, but has: " + ((Anchor[]) o).length);
+                               } else {
+                                       aa = (Anchor[]) o;
+                               }
+                       }
+               }
+       }
+
+       private abstract static class MarkToBaseSubtable extends GlyphPositioningSubtable {
+
+               MarkToBaseSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GPOS_LOOKUP_TYPE_MARK_TO_BASE;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof MarkToBaseSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean position(GlyphPositioningState ps) {
+                       boolean applied = false;
+                       int giMark = ps.getGlyph();
+                       int ciMark;
+                       if ((ciMark = getCoverageIndex(giMark)) >= 0) {
+                               MarkAnchor ma = getMarkAnchor(ciMark, giMark);
+                               if (ma != null) {
+                                       for (int i = 0, n = ps.getPosition(); i < n; i++) {
+                                               int gi = ps.getGlyph(-(i + 1));
+                                               if (ps.isMark(gi)) {
+                                                       continue;
+                                               } else {
+                                                       Anchor a = getBaseAnchor(gi, ma.getMarkClass());
+                                                       if (a != null) {
+                                                               Value v = a.getAlignmentAdjustment(ma);
+                                                               // start experimental fix for END OF AYAH in Lateef/Scheherazade
+                                                               int[] aa = ps.getAdjustment();
+                                                               if (aa[2] == 0) {
+                                                                       v.adjust(0, 0, -ps.getWidth(giMark), 0);
+                                                               }
+                                                               // end experimental fix for END OF AYAH in Lateef/Scheherazade
+                                                               if (ps.adjust(v)) {
+                                                                       ps.setAdjusted(true);
+                                                               }
+                                                       }
+                                                       ps.consume(1);
+                                                       applied = true;
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+                       return applied;
+               }
+
+               /**
+                * Obtain mark anchor associated with mark coverage index.
+                *
+                * @param ciMark coverage index
+                * @param giMark input glyph index of mark glyph
+                * @return mark anchor or null if none applies
+                */
+               public abstract MarkAnchor getMarkAnchor(int ciMark, int giMark);
+
+               /**
+                * Obtain anchor associated with base glyph index and mark class.
+                *
+                * @param giBase    input glyph index of base glyph
+                * @param markClass class number of mark glyph
+                * @return anchor or null if none applies
+                */
+               public abstract Anchor getBaseAnchor(int giBase, int markClass);
+
+               static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                          List entries) {
+                       if (format == 1) {
+                               return new MarkToBaseSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class MarkToBaseSubtableFormat1 extends MarkToBaseSubtable {
+
+               private GlyphCoverageTable bct;                 // base coverage table
+               private int nmc;                                // mark class count
+               private MarkAnchor[] maa;                       // mark anchor array, ordered by mark coverage index
+               private Anchor[][] bam;
+               // base anchor matrix, ordered by base coverage index, then by mark class
+
+               MarkToBaseSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                 List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if ((bct != null) && (maa != null) && (nmc > 0) && (bam != null)) {
+                               List entries = new ArrayList(4);
+                               entries.add(bct);
+                               entries.add(Integer.valueOf(nmc));
+                               entries.add(maa);
+                               entries.add(bam);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public MarkAnchor getMarkAnchor(int ciMark, int giMark) {
+                       if ((maa != null) && (ciMark < maa.length)) {
+                               return maa[ciMark];
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public Anchor getBaseAnchor(int giBase, int markClass) {
+                       int ciBase;
+                       if ((bct != null) && ((ciBase = bct.getCoverageIndex(giBase)) >= 0)) {
+                               if ((bam != null) && (ciBase < bam.length)) {
+                                       Anchor[] ba = bam[ciBase];
+                                       if ((ba != null) && (markClass < ba.length)) {
+                                               return ba[markClass];
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 4) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 4 entries");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof GlyphCoverageTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an GlyphCoverageTable, but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else {
+                                       bct = (GlyphCoverageTable) o;
+                               }
+                               if (((o = entries.get(1)) == null) || !(o instanceof Integer)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, second entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       nmc = ((Integer) (o)).intValue();
+                               }
+                               if (((o = entries.get(2)) == null) || !(o instanceof MarkAnchor[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, third entry must be a MarkAnchor[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       maa = (MarkAnchor[]) o;
+                               }
+                               if (((o = entries.get(3)) == null) || !(o instanceof Anchor[][])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, fourth entry must be a Anchor[][], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       bam = (Anchor[][]) o;
+                               }
+                       }
+               }
+       }
+
+       private abstract static class MarkToLigatureSubtable extends GlyphPositioningSubtable {
+
+               MarkToLigatureSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof MarkToLigatureSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean position(GlyphPositioningState ps) {
+                       boolean applied = false;
+                       int giMark = ps.getGlyph();
+                       int ciMark;
+                       if ((ciMark = getCoverageIndex(giMark)) >= 0) {
+                               MarkAnchor ma = getMarkAnchor(ciMark, giMark);
+                               int mxc = getMaxComponentCount();
+                               if (ma != null) {
+                                       for (int i = 0, n = ps.getPosition(); i < n; i++) {
+                                               int gi = ps.getGlyph(-(i + 1));
+                                               if (ps.isMark(gi)) {
+                                                       continue;
+                                               } else {
+                                                       Anchor a = getLigatureAnchor(gi, mxc, i, ma.getMarkClass());
+                                                       if (a != null) {
+                                                               if (ps.adjust(a.getAlignmentAdjustment(ma))) {
+                                                                       ps.setAdjusted(true);
+                                                               }
+                                                       }
+                                                       ps.consume(1);
+                                                       applied = true;
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+                       return applied;
+               }
+
+               /**
+                * Obtain mark anchor associated with mark coverage index.
+                *
+                * @param ciMark coverage index
+                * @param giMark input glyph index of mark glyph
+                * @return mark anchor or null if none applies
+                */
+               public abstract MarkAnchor getMarkAnchor(int ciMark, int giMark);
+
+               /**
+                * Obtain maximum component count.
+                *
+                * @return maximum component count (>=0)
+                */
+               public abstract int getMaxComponentCount();
+
+               /**
+                * Obtain anchor associated with ligature glyph index and mark class.
+                *
+                * @param giLig         input glyph index of ligature glyph
+                * @param maxComponents maximum component count
+                * @param component     component number (0...maxComponents-1)
+                * @param markClass     class number of mark glyph
+                * @return anchor or null if none applies
+                */
+               public abstract Anchor getLigatureAnchor(int giLig, int maxComponents, int component, int markClass);
+
+               static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                          List entries) {
+                       if (format == 1) {
+                               return new MarkToLigatureSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class MarkToLigatureSubtableFormat1 extends MarkToLigatureSubtable {
+
+               private GlyphCoverageTable lct;                 // ligature coverage table
+               private int nmc;                                // mark class count
+               private int mxc;                                // maximum ligature component count
+               private MarkAnchor[] maa;                       // mark anchor array, ordered by mark coverage index
+               private Anchor[][][] lam;
+               // ligature anchor matrix, ordered by ligature coverage index, then ligature component, then mark class
+
+               MarkToLigatureSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                         List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (lam != null) {
+                               List entries = new ArrayList(5);
+                               entries.add(lct);
+                               entries.add(Integer.valueOf(nmc));
+                               entries.add(Integer.valueOf(mxc));
+                               entries.add(maa);
+                               entries.add(lam);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public MarkAnchor getMarkAnchor(int ciMark, int giMark) {
+                       if ((maa != null) && (ciMark < maa.length)) {
+                               return maa[ciMark];
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getMaxComponentCount() {
+                       return mxc;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public Anchor getLigatureAnchor(int giLig, int maxComponents, int component, int markClass) {
+                       int ciLig;
+                       if ((lct != null) && ((ciLig = lct.getCoverageIndex(giLig)) >= 0)) {
+                               if ((lam != null) && (ciLig < lam.length)) {
+                                       Anchor[][] lcm = lam[ciLig];
+                                       if (component < maxComponents) {
+                                               Anchor[] la = lcm[component];
+                                               if ((la != null) && (markClass < la.length)) {
+                                                       return la[markClass];
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 5) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 5 entries");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof GlyphCoverageTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an GlyphCoverageTable, but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else {
+                                       lct = (GlyphCoverageTable) o;
+                               }
+                               if (((o = entries.get(1)) == null) || !(o instanceof Integer)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, second entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       nmc = ((Integer) (o)).intValue();
+                               }
+                               if (((o = entries.get(2)) == null) || !(o instanceof Integer)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, third entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       mxc = ((Integer) (o)).intValue();
+                               }
+                               if (((o = entries.get(3)) == null) || !(o instanceof MarkAnchor[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, fourth entry must be a MarkAnchor[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       maa = (MarkAnchor[]) o;
+                               }
+                               if (((o = entries.get(4)) == null) || !(o instanceof Anchor[][][])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, fifth entry must be a Anchor[][][], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       lam = (Anchor[][][]) o;
+                               }
+                       }
+               }
+       }
+
+       private abstract static class MarkToMarkSubtable extends GlyphPositioningSubtable {
+
+               MarkToMarkSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GPOS_LOOKUP_TYPE_MARK_TO_MARK;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof MarkToMarkSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean position(GlyphPositioningState ps) {
+                       boolean applied = false;
+                       int giMark1 = ps.getGlyph();
+                       int ciMark1;
+                       if ((ciMark1 = getCoverageIndex(giMark1)) >= 0) {
+                               MarkAnchor ma = getMark1Anchor(ciMark1, giMark1);
+                               if (ma != null) {
+                                       if (ps.hasPrev()) {
+                                               Anchor a = getMark2Anchor(ps.getGlyph(-1), ma.getMarkClass());
+                                               if (a != null) {
+                                                       if (ps.adjust(a.getAlignmentAdjustment(ma))) {
+                                                               ps.setAdjusted(true);
+                                                       }
+                                               }
+                                               ps.consume(1);
+                                               applied = true;
+                                       }
+                               }
+                       }
+                       return applied;
+               }
+
+               /**
+                * Obtain mark 1 anchor associated with mark 1 coverage index.
+                *
+                * @param ciMark1 mark 1 coverage index
+                * @param giMark1 input glyph index of mark 1 glyph
+                * @return mark 1 anchor or null if none applies
+                */
+               public abstract MarkAnchor getMark1Anchor(int ciMark1, int giMark1);
+
+               /**
+                * Obtain anchor associated with mark 2 glyph index and mark 1 class.
+                *
+                * @param giBase    input glyph index of mark 2 glyph
+                * @param markClass class number of mark 1 glyph
+                * @return anchor or null if none applies
+                */
+               public abstract Anchor getMark2Anchor(int giBase, int markClass);
+
+               static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                          List entries) {
+                       if (format == 1) {
+                               return new MarkToMarkSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class MarkToMarkSubtableFormat1 extends MarkToMarkSubtable {
+
+               private GlyphCoverageTable mct2;                // mark 2 coverage table
+               private int nmc;                                // mark class count
+               private MarkAnchor[] maa;                       // mark1 anchor array, ordered by mark1 coverage index
+               private Anchor[][] mam;
+               // mark2 anchor matrix, ordered by mark2 coverage index, then by mark1 class
+
+               MarkToMarkSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                 List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if ((mct2 != null) && (maa != null) && (nmc > 0) && (mam != null)) {
+                               List entries = new ArrayList(4);
+                               entries.add(mct2);
+                               entries.add(Integer.valueOf(nmc));
+                               entries.add(maa);
+                               entries.add(mam);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public MarkAnchor getMark1Anchor(int ciMark1, int giMark1) {
+                       if ((maa != null) && (ciMark1 < maa.length)) {
+                               return maa[ciMark1];
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public Anchor getMark2Anchor(int giMark2, int markClass) {
+                       int ciMark2;
+                       if ((mct2 != null) && ((ciMark2 = mct2.getCoverageIndex(giMark2)) >= 0)) {
+                               if ((mam != null) && (ciMark2 < mam.length)) {
+                                       Anchor[] ma = mam[ciMark2];
+                                       if ((ma != null) && (markClass < ma.length)) {
+                                               return ma[markClass];
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 4) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 4 entries");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof GlyphCoverageTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an GlyphCoverageTable, but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else {
+                                       mct2 = (GlyphCoverageTable) o;
+                               }
+                               if (((o = entries.get(1)) == null) || !(o instanceof Integer)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, second entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       nmc = ((Integer) (o)).intValue();
+                               }
+                               if (((o = entries.get(2)) == null) || !(o instanceof MarkAnchor[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, third entry must be a MarkAnchor[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       maa = (MarkAnchor[]) o;
+                               }
+                               if (((o = entries.get(3)) == null) || !(o instanceof Anchor[][])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, fourth entry must be a Anchor[][], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       mam = (Anchor[][]) o;
+                               }
+                       }
+               }
+       }
+
+       private abstract static class ContextualSubtable extends GlyphPositioningSubtable {
+
+               ContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GPOS_LOOKUP_TYPE_CONTEXTUAL;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof ContextualSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean position(GlyphPositioningState ps) {
+                       boolean applied = false;
+                       int gi = ps.getGlyph();
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) >= 0) {
+                               int[] rv = new int[1];
+                               RuleLookup[] la = getLookups(ci, gi, ps, rv);
+                               if (la != null) {
+                                       ps.apply(la, rv[0]);
+                                       applied = true;
+                               }
+                       }
+                       return applied;
+               }
+
+               /**
+                * Obtain rule lookups set associated current input glyph context.
+                *
+                * @param ci coverage index of glyph at current position
+                * @param gi glyph index of glyph at current position
+                * @param ps glyph positioning state
+                * @param rv array of ints used to receive multiple return values, must be of length 1 or greater,
+                *           where the first entry is used to return the input sequence length of the matched rule
+                * @return array of rule lookups or null if none applies
+                */
+               public abstract RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv);
+
+               static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                          List entries) {
+                       if (format == 1) {
+                               return new ContextualSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 2) {
+                               return new ContextualSubtableFormat2(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 3) {
+                               return new ContextualSubtableFormat3(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class ContextualSubtableFormat1 extends ContextualSubtable {
+
+               private RuleSet[] rsa;                          // rule set array, ordered by glyph coverage index
+
+               ContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                 List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
+                       assert ps != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedGlyphSequenceRule)) {
+                                                       ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r;
+                                                       int[] iga = cr.getGlyphs(gi);
+                                                       if (matches(ps, iga, 0, rv)) {
+                                                               return r.getLookups();
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               static boolean matches(GlyphPositioningState ps, int[] glyphs, int offset, int[] rv) {
+                       if ((glyphs == null) || (glyphs.length == 0)) {
+                               return true;                            // match null or empty glyph sequence
+                       } else {
+                               boolean reverse = offset < 0;
+                               GlyphTester ignores = ps.getIgnoreDefault();
+                               int[] counts = ps.getGlyphsAvailable(offset, reverse, ignores);
+                               int nga = counts[0];
+                               int ngm = glyphs.length;
+                               if (nga < ngm) {
+                                       return false;                       // insufficient glyphs available to match
+                               } else {
+                                       int[] ga = ps.getGlyphs(offset, ngm, reverse, ignores, null, counts);
+                                       for (int k = 0; k < ngm; k++) {
+                                               if (ga[k] != glyphs[k]) {
+                                                       return false;               // match fails at ga [ k ]
+                                               }
+                                       }
+                                       if (rv != null) {
+                                               rv[0] = counts[0] + counts[1];
+                                       }
+                                       return true;                        // all glyphs match
+                               }
+                       }
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                               }
+                       }
+               }
+       }
+
+       private static class ContextualSubtableFormat2 extends ContextualSubtable {
+
+               private GlyphClassTable cdt;                    // class def table
+               private int ngc;                                // class set count
+               private RuleSet[] rsa;                          // rule set array, ordered by class number [0...ngc - 1]
+
+               ContextualSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                 List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(3);
+                               entries.add(cdt);
+                               entries.add(Integer.valueOf(ngc));
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
+                       assert ps != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedClassSequenceRule)) {
+                                                       ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r;
+                                                       int[] ca = cr.getClasses(cdt.getClassIndex(gi, ps.getClassMatchSet(gi)));
+                                                       if (matches(ps, cdt, ca, 0, rv)) {
+                                                               return r.getLookups();
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               static boolean matches(GlyphPositioningState ps, GlyphClassTable cdt, int[] classes, int offset, int[] rv) {
+                       if ((cdt == null) || (classes == null) || (classes.length == 0)) {
+                               return true;                            // match null class definitions, null or empty class sequence
+                       } else {
+                               boolean reverse = offset < 0;
+                               GlyphTester ignores = ps.getIgnoreDefault();
+                               int[] counts = ps.getGlyphsAvailable(offset, reverse, ignores);
+                               int nga = counts[0];
+                               int ngm = classes.length;
+                               if (nga < ngm) {
+                                       return false;                       // insufficient glyphs available to match
+                               } else {
+                                       int[] ga = ps.getGlyphs(offset, ngm, reverse, ignores, null, counts);
+                                       for (int k = 0; k < ngm; k++) {
+                                               int gi = ga[k];
+                                               int ms = ps.getClassMatchSet(gi);
+                                               int gc = cdt.getClassIndex(gi, ms);
+                                               if ((gc < 0) || (gc >= cdt.getClassSize(ms))) {
+                                                       return false;               // none or invalid class fails mat ch
+                                               } else if (gc != classes[k]) {
+                                                       return false;               // match fails at ga [ k ]
+                                               }
+                                       }
+                                       if (rv != null) {
+                                               rv[0] = counts[0] + counts[1];
+                                       }
+                                       return true;                        // all glyphs match
+                               }
+                       }
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 3) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 3 entries");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof GlyphClassTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an GlyphClassTable, but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else {
+                                       cdt = (GlyphClassTable) o;
+                               }
+                               if (((o = entries.get(1)) == null) || !(o instanceof Integer)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, second entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       ngc = ((Integer) (o)).intValue();
+                               }
+                               if (((o = entries.get(2)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, third entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                                       if (rsa.length != ngc) {
+                                               throw new AdvancedTypographicTableFormatException(
+                                                               "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes");
+                                       }
+                               }
+                       }
+               }
+       }
+
+       private static class ContextualSubtableFormat3 extends ContextualSubtable {
+
+               private RuleSet[] rsa;                          // rule set array, containing a single rule set
+
+               ContextualSubtableFormat3(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                 List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
+                       assert ps != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedCoverageSequenceRule)) {
+                                                       ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r;
+                                                       GlyphCoverageTable[] gca = cr.getCoverages();
+                                                       if (matches(ps, gca, 0, rv)) {
+                                                               return r.getLookups();
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               static boolean matches(GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv) {
+                       if ((gca == null) || (gca.length == 0)) {
+                               return true;                            // match null or empty coverage array
+                       } else {
+                               boolean reverse = offset < 0;
+                               GlyphTester ignores = ps.getIgnoreDefault();
+                               int[] counts = ps.getGlyphsAvailable(offset, reverse, ignores);
+                               int nga = counts[0];
+                               int ngm = gca.length;
+                               if (nga < ngm) {
+                                       return false;                       // insufficient glyphs available to match
+                               } else {
+                                       int[] ga = ps.getGlyphs(offset, ngm, reverse, ignores, null, counts);
+                                       for (int k = 0; k < ngm; k++) {
+                                               GlyphCoverageTable ct = gca[k];
+                                               if (ct != null) {
+                                                       if (ct.getCoverageIndex(ga[k]) < 0) {
+                                                               return false;           // match fails at ga [ k ]
+                                                       }
+                                               }
+                                       }
+                                       if (rv != null) {
+                                               rv[0] = counts[0] + counts[1];
+                                       }
+                                       return true;                        // all glyphs match
+                               }
+                       }
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                               }
+                       }
+               }
+       }
+
+       private abstract static class ChainedContextualSubtable extends GlyphPositioningSubtable {
+
+               ChainedContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                 List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof ChainedContextualSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean position(GlyphPositioningState ps) {
+                       boolean applied = false;
+                       int gi = ps.getGlyph();
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) >= 0) {
+                               int[] rv = new int[1];
+                               RuleLookup[] la = getLookups(ci, gi, ps, rv);
+                               if (la != null) {
+                                       ps.apply(la, rv[0]);
+                                       applied = true;
+                               }
+                       }
+                       return applied;
+               }
+
+               /**
+                * Obtain rule lookups set associated current input glyph context.
+                *
+                * @param ci coverage index of glyph at current position
+                * @param gi glyph index of glyph at current position
+                * @param ps glyph positioning state
+                * @param rv array of ints used to receive multiple return values, must be of length 1 or greater,
+                *           where the first entry is used to return the input sequence length of the matched rule
+                * @return array of rule lookups or null if none applies
+                */
+               public abstract RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv);
+
+               static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                          List entries) {
+                       if (format == 1) {
+                               return new ChainedContextualSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 2) {
+                               return new ChainedContextualSubtableFormat2(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 3) {
+                               return new ChainedContextualSubtableFormat3(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class ChainedContextualSubtableFormat1 extends ChainedContextualSubtable {
+
+               private RuleSet[] rsa;                          // rule set array, ordered by glyph coverage index
+
+               ChainedContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
+                       assert ps != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedGlyphSequenceRule)) {
+                                                       ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r;
+                                                       int[] iga = cr.getGlyphs(gi);
+                                                       if (matches(ps, iga, 0, rv)) {
+                                                               int[] bga = cr.getBacktrackGlyphs();
+                                                               if (matches(ps, bga, -1, null)) {
+                                                                       int[] lga = cr.getLookaheadGlyphs();
+                                                                       if (matches(ps, lga, rv[0], null)) {
+                                                                               return r.getLookups();
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private boolean matches(GlyphPositioningState ps, int[] glyphs, int offset, int[] rv) {
+                       return ContextualSubtableFormat1.matches(ps, glyphs, offset, rv);
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                               }
+                       }
+               }
+       }
+
+       private static class ChainedContextualSubtableFormat2 extends ChainedContextualSubtable {
+
+               private GlyphClassTable icdt;                   // input class def table
+               private GlyphClassTable bcdt;                   // backtrack class def table
+               private GlyphClassTable lcdt;                   // lookahead class def table
+               private int ngc;                                // class set count
+               private RuleSet[] rsa;                          // rule set array, ordered by class number [0...ngc - 1]
+
+               ChainedContextualSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(5);
+                               entries.add(icdt);
+                               entries.add(bcdt);
+                               entries.add(lcdt);
+                               entries.add(Integer.valueOf(ngc));
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
+                       assert ps != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedClassSequenceRule)) {
+                                                       ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r;
+                                                       int[] ica = cr.getClasses(icdt.getClassIndex(gi, ps.getClassMatchSet(gi)));
+                                                       if (matches(ps, icdt, ica, 0, rv)) {
+                                                               int[] bca = cr.getBacktrackClasses();
+                                                               if (matches(ps, bcdt, bca, -1, null)) {
+                                                                       int[] lca = cr.getLookaheadClasses();
+                                                                       if (matches(ps, lcdt, lca, rv[0], null)) {
+                                                                               return r.getLookups();
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private boolean matches(GlyphPositioningState ps, GlyphClassTable cdt, int[] classes, int offset, int[] rv) {
+                       return ContextualSubtableFormat2.matches(ps, cdt, classes, offset, rv);
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 5) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 5 entries");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof GlyphClassTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an GlyphClassTable, but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else {
+                                       icdt = (GlyphClassTable) o;
+                               }
+                               if (((o = entries.get(1)) != null) && !(o instanceof GlyphClassTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, second entry must be an GlyphClassTable, but is: " + o.getClass());
+                               } else {
+                                       bcdt = (GlyphClassTable) o;
+                               }
+                               if (((o = entries.get(2)) != null) && !(o instanceof GlyphClassTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, third entry must be an GlyphClassTable, but is: " + o.getClass());
+                               } else {
+                                       lcdt = (GlyphClassTable) o;
+                               }
+                               if (((o = entries.get(3)) == null) || !(o instanceof Integer)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, fourth entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       ngc = ((Integer) (o)).intValue();
+                               }
+                               if (((o = entries.get(4)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, fifth entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                                       if (rsa.length != ngc) {
+                                               throw new AdvancedTypographicTableFormatException(
+                                                               "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes");
+                                       }
+                               }
+                       }
+               }
+       }
+
+       private static class ChainedContextualSubtableFormat3 extends ChainedContextualSubtable {
+
+               private RuleSet[] rsa;                          // rule set array, containing a single rule set
+
+               ChainedContextualSubtableFormat3(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
+                       assert ps != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedCoverageSequenceRule)) {
+                                                       ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r;
+                                                       GlyphCoverageTable[] igca = cr.getCoverages();
+                                                       if (matches(ps, igca, 0, rv)) {
+                                                               GlyphCoverageTable[] bgca = cr.getBacktrackCoverages();
+                                                               if (matches(ps, bgca, -1, null)) {
+                                                                       GlyphCoverageTable[] lgca = cr.getLookaheadCoverages();
+                                                                       if (matches(ps, lgca, rv[0], null)) {
+                                                                               return r.getLookups();
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private boolean matches(GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv) {
+                       return ContextualSubtableFormat3.matches(ps, gca, offset, rv);
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                               }
+                       }
+               }
+       }
+
+       /**
+        * The <code>DeviceTable</code> class implements a positioning device table record, comprising
+        * adjustments to be made to scaled design units according to the scaled size.
+        */
+       public static class DeviceTable {
+
+               private final int startSize;
+               private final int endSize;
+               private final int[] deltas;
+
+               /**
+                * Instantiate a DeviceTable.
+                *
+                * @param startSize the
+                * @param endSize   the ending (scaled) size
+                * @param deltas    adjustments for each scaled size
+                */
+               public DeviceTable(int startSize, int endSize, int[] deltas) {
+                       assert startSize >= 0;
+                       assert startSize <= endSize;
+                       assert deltas != null;
+                       assert deltas.length == (endSize - startSize) + 1;
+                       this.startSize = startSize;
+                       this.endSize = endSize;
+                       this.deltas = deltas;
+               }
+
+               /**
+                * @return the start size
+                */
+               public int getStartSize() {
+                       return startSize;
+               }
+
+               /**
+                * @return the end size
+                */
+               public int getEndSize() {
+                       return endSize;
+               }
+
+               /**
+                * @return the deltas
+                */
+               public int[] getDeltas() {
+                       return deltas;
+               }
+
+               /**
+                * Find device adjustment.
+                *
+                * @param fontSize the font size to search for
+                * @return an adjustment if font size matches an entry
+                */
+               public int findAdjustment(int fontSize) {
+                       // [TODO] at present, assumes that 1 device unit equals one point
+                       int fs = fontSize / 1000;
+                       if (fs < startSize) {
+                               return 0;
+                       } else if (fs <= endSize) {
+                               return deltas[fs - startSize] * 1000;
+                       } else {
+                               return 0;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       return "{ start = " + startSize + ", end = " + endSize + ", deltas = " + Arrays.toString(deltas) + "}";
+               }
+
+       }
+
+       /**
+        * The <code>Value</code> class implements a positioning value record, comprising placement
+        * and advancement information in X and Y axes, and optionally including device data used to
+        * perform device (grid-fitted) specific fine grain adjustments.
+        */
+       public static class Value {
+
+               /**
+                * X_PLACEMENT value format flag
+                */
+               public static final int X_PLACEMENT = 0x0001;
+               /**
+                * Y_PLACEMENT value format flag
+                */
+               public static final int Y_PLACEMENT = 0x0002;
+               /**
+                * X_ADVANCE value format flag
+                */
+               public static final int X_ADVANCE = 0x0004;
+               /**
+                * Y_ADVANCE value format flag
+                */
+               public static final int Y_ADVANCE = 0x0008;
+               /**
+                * X_PLACEMENT_DEVICE value format flag
+                */
+               public static final int X_PLACEMENT_DEVICE = 0x0010;
+               /**
+                * Y_PLACEMENT_DEVICE value format flag
+                */
+               public static final int Y_PLACEMENT_DEVICE = 0x0020;
+               /**
+                * X_ADVANCE_DEVICE value format flag
+                */
+               public static final int X_ADVANCE_DEVICE = 0x0040;
+               /**
+                * Y_ADVANCE_DEVICE value format flag
+                */
+               public static final int Y_ADVANCE_DEVICE = 0x0080;
+
+               /**
+                * X_PLACEMENT value index (within adjustments arrays)
+                */
+               public static final int IDX_X_PLACEMENT = 0;
+               /**
+                * Y_PLACEMENT value index (within adjustments arrays)
+                */
+               public static final int IDX_Y_PLACEMENT = 1;
+               /**
+                * X_ADVANCE value index (within adjustments arrays)
+                */
+               public static final int IDX_X_ADVANCE = 2;
+               /**
+                * Y_ADVANCE value index (within adjustments arrays)
+                */
+               public static final int IDX_Y_ADVANCE = 3;
+
+               private int xPlacement;                         // x placement
+               private int yPlacement;                         // y placement
+               private int xAdvance;                           // x advance
+               private int yAdvance;                           // y advance
+               private final DeviceTable xPlaDevice;           // x placement device table
+               private final DeviceTable yPlaDevice;           // y placement device table
+               private final DeviceTable xAdvDevice;           // x advance device table
+               private final DeviceTable yAdvDevice;           // x advance device table
+
+               /**
+                * Instantiate a Value.
+                *
+                * @param xPlacement the x placement or zero
+                * @param yPlacement the y placement or zero
+                * @param xAdvance   the x advance or zero
+                * @param yAdvance   the y advance or zero
+                * @param xPlaDevice the x placement device table or null
+                * @param yPlaDevice the y placement device table or null
+                * @param xAdvDevice the x advance device table or null
+                * @param yAdvDevice the y advance device table or null
+                */
+               public Value(int xPlacement, int yPlacement, int xAdvance, int yAdvance, DeviceTable xPlaDevice,
+                                        DeviceTable yPlaDevice, DeviceTable xAdvDevice, DeviceTable yAdvDevice) {
+                       this.xPlacement = xPlacement;
+                       this.yPlacement = yPlacement;
+                       this.xAdvance = xAdvance;
+                       this.yAdvance = yAdvance;
+                       this.xPlaDevice = xPlaDevice;
+                       this.yPlaDevice = yPlaDevice;
+                       this.xAdvDevice = xAdvDevice;
+                       this.yAdvDevice = yAdvDevice;
+               }
+
+               /**
+                * @return the x placement
+                */
+               public int getXPlacement() {
+                       return xPlacement;
+               }
+
+               /**
+                * @return the y placement
+                */
+               public int getYPlacement() {
+                       return yPlacement;
+               }
+
+               /**
+                * @return the x advance
+                */
+               public int getXAdvance() {
+                       return xAdvance;
+               }
+
+               /**
+                * @return the y advance
+                */
+               public int getYAdvance() {
+                       return yAdvance;
+               }
+
+               /**
+                * @return the x placement device table
+                */
+               public DeviceTable getXPlaDevice() {
+                       return xPlaDevice;
+               }
+
+               /**
+                * @return the y placement device table
+                */
+               public DeviceTable getYPlaDevice() {
+                       return yPlaDevice;
+               }
+
+               /**
+                * @return the x advance device table
+                */
+               public DeviceTable getXAdvDevice() {
+                       return xAdvDevice;
+               }
+
+               /**
+                * @return the y advance device table
+                */
+               public DeviceTable getYAdvDevice() {
+                       return yAdvDevice;
+               }
+
+               /**
+                * Apply value to specific adjustments to without use of device table adjustments.
+                *
+                * @param xPlacement the x placement or zero
+                * @param yPlacement the y placement or zero
+                * @param xAdvance   the x advance or zero
+                * @param yAdvance   the y advance or zero
+                */
+               public void adjust(int xPlacement, int yPlacement, int xAdvance, int yAdvance) {
+                       this.xPlacement += xPlacement;
+                       this.yPlacement += yPlacement;
+                       this.xAdvance += xAdvance;
+                       this.yAdvance += yAdvance;
+               }
+
+               /**
+                * Apply value to adjustments using font size for device table adjustments.
+                *
+                * @param adjustments array of four integers containing X,Y placement and X,Y advance adjustments
+                * @param fontSize    font size for device table adjustments
+                * @return true if some adjustment was made
+                */
+               public boolean adjust(int[] adjustments, int fontSize) {
+                       boolean adjust = false;
+                       int dv;
+                       if ((dv = xPlacement) != 0) {
+                               adjustments[IDX_X_PLACEMENT] += dv;
+                               adjust = true;
+                       }
+                       if ((dv = yPlacement) != 0) {
+                               adjustments[IDX_Y_PLACEMENT] += dv;
+                               adjust = true;
+                       }
+                       if ((dv = xAdvance) != 0) {
+                               adjustments[IDX_X_ADVANCE] += dv;
+                               adjust = true;
+                       }
+                       if ((dv = yAdvance) != 0) {
+                               adjustments[IDX_Y_ADVANCE] += dv;
+                               adjust = true;
+                       }
+                       if (fontSize != 0) {
+                               DeviceTable dt;
+                               if ((dt = xPlaDevice) != null) {
+                                       if ((dv = dt.findAdjustment(fontSize)) != 0) {
+                                               adjustments[IDX_X_PLACEMENT] += dv;
+                                               adjust = true;
+                                       }
+                               }
+                               if ((dt = yPlaDevice) != null) {
+                                       if ((dv = dt.findAdjustment(fontSize)) != 0) {
+                                               adjustments[IDX_Y_PLACEMENT] += dv;
+                                               adjust = true;
+                                       }
+                               }
+                               if ((dt = xAdvDevice) != null) {
+                                       if ((dv = dt.findAdjustment(fontSize)) != 0) {
+                                               adjustments[IDX_X_ADVANCE] += dv;
+                                               adjust = true;
+                                       }
+                               }
+                               if ((dt = yAdvDevice) != null) {
+                                       if ((dv = dt.findAdjustment(fontSize)) != 0) {
+                                               adjustments[IDX_Y_ADVANCE] += dv;
+                                               adjust = true;
+                                       }
+                               }
+                       }
+                       return adjust;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       boolean first = true;
+                       sb.append("{ ");
+                       if (xPlacement != 0) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("xPlacement = " + xPlacement);
+                       }
+                       if (yPlacement != 0) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("yPlacement = " + yPlacement);
+                       }
+                       if (xAdvance != 0) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("xAdvance = " + xAdvance);
+                       }
+                       if (yAdvance != 0) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("yAdvance = " + yAdvance);
+                       }
+                       if (xPlaDevice != null) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("xPlaDevice = " + xPlaDevice);
+                       }
+                       if (yPlaDevice != null) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("xPlaDevice = " + yPlaDevice);
+                       }
+                       if (xAdvDevice != null) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("xAdvDevice = " + xAdvDevice);
+                       }
+                       if (yAdvDevice != null) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("xAdvDevice = " + yAdvDevice);
+                       }
+                       sb.append(" }");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>PairValues</code> class implements a pair value record, comprising a glyph id (or zero)
+        * and two optional positioning values.
+        */
+       public static class PairValues {
+
+               private final int glyph;                        // glyph id (or 0)
+               private final Value value1;                     // value for first glyph in pair (or null)
+               private final Value value2;                     // value for second glyph in pair (or null)
+
+               /**
+                * Instantiate a PairValues.
+                *
+                * @param glyph  the glyph id (or zero)
+                * @param value1 the value of the first glyph in pair (or null)
+                * @param value2 the value of the second glyph in pair (or null)
+                */
+               public PairValues(int glyph, Value value1, Value value2) {
+                       assert glyph >= 0;
+                       this.glyph = glyph;
+                       this.value1 = value1;
+                       this.value2 = value2;
+               }
+
+               /**
+                * @return the glyph id
+                */
+               public int getGlyph() {
+                       return glyph;
+               }
+
+               /**
+                * @return the first value
+                */
+               public Value getValue1() {
+                       return value1;
+               }
+
+               /**
+                * @return the second value
+                */
+               public Value getValue2() {
+                       return value2;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       boolean first = true;
+                       sb.append("{ ");
+                       if (glyph != 0) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("glyph = " + glyph);
+                       }
+                       if (value1 != null) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("value1 = " + value1);
+                       }
+                       if (value2 != null) {
+                               if (!first) {
+                                       sb.append(", ");
+                               } else {
+                                       first = false;
+                               }
+                               sb.append("value2 = " + value2);
+                       }
+                       sb.append(" }");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>Anchor</code> class implements a anchor record, comprising an X,Y coordinate pair,
+        * an optional anchor point index (or -1), and optional X or Y device tables (or null if absent).
+        */
+       public static class Anchor {
+
+               private final int x;                            // xCoordinate (in design units)
+               private final int y;                            // yCoordinate (in design units)
+               private final int anchorPoint;                  // anchor point index (or -1)
+               private final DeviceTable xDevice;              // x device table
+               private final DeviceTable yDevice;              // y device table
+
+               /**
+                * Instantiate an Anchor (format 1).
+                *
+                * @param x the x coordinate
+                * @param y the y coordinate
+                */
+               public Anchor(int x, int y) {
+                       this(x, y, -1, null, null);
+               }
+
+               /**
+                * Instantiate an Anchor (format 2).
+                *
+                * @param x           the x coordinate
+                * @param y           the y coordinate
+                * @param anchorPoint anchor index (or -1)
+                */
+               public Anchor(int x, int y, int anchorPoint) {
+                       this(x, y, anchorPoint, null, null);
+               }
+
+               /**
+                * Instantiate an Anchor (format 3).
+                *
+                * @param x       the x coordinate
+                * @param y       the y coordinate
+                * @param xDevice the x device table (or null if not present)
+                * @param yDevice the y device table (or null if not present)
+                */
+               public Anchor(int x, int y, DeviceTable xDevice, DeviceTable yDevice) {
+                       this(x, y, -1, xDevice, yDevice);
+               }
+
+               /**
+                * Instantiate an Anchor based on an existing anchor.
+                *
+                * @param a the existing anchor
+                */
+               protected Anchor(Anchor a) {
+                       this(a.x, a.y, a.anchorPoint, a.xDevice, a.yDevice);
+               }
+
+               private Anchor(int x, int y, int anchorPoint, DeviceTable xDevice, DeviceTable yDevice) {
+                       assert (anchorPoint >= 0) || (anchorPoint == -1);
+                       this.x = x;
+                       this.y = y;
+                       this.anchorPoint = anchorPoint;
+                       this.xDevice = xDevice;
+                       this.yDevice = yDevice;
+               }
+
+               /**
+                * @return the x coordinate
+                */
+               public int getX() {
+                       return x;
+               }
+
+               /**
+                * @return the y coordinate
+                */
+               public int getY() {
+                       return y;
+               }
+
+               /**
+                * @return the anchor point index (or -1 if not specified)
+                */
+               public int getAnchorPoint() {
+                       return anchorPoint;
+               }
+
+               /**
+                * @return the x device table (or null if not specified)
+                */
+               public DeviceTable getXDevice() {
+                       return xDevice;
+               }
+
+               /**
+                * @return the y device table (or null if not specified)
+                */
+               public DeviceTable getYDevice() {
+                       return yDevice;
+               }
+
+               /**
+                * Obtain adjustment value required to align the specified anchor
+                * with this anchor.
+                *
+                * @param a the anchor to align
+                * @return the adjustment value needed to effect alignment
+                */
+               public Value getAlignmentAdjustment(Anchor a) {
+                       assert a != null;
+                       // TODO - handle anchor point
+                       // TODO - handle device tables
+                       return new Value(x - a.x, y - a.y, 0, 0, null, null, null, null);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{ [" + x + "," + y + "]");
+                       if (anchorPoint != -1) {
+                               sb.append(", anchorPoint = " + anchorPoint);
+                       }
+                       if (xDevice != null) {
+                               sb.append(", xDevice = " + xDevice);
+                       }
+                       if (yDevice != null) {
+                               sb.append(", yDevice = " + yDevice);
+                       }
+                       sb.append(" }");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>MarkAnchor</code> class is a subclass of the <code>Anchor</code> class, adding a mark
+        * class designation.
+        */
+       public static class MarkAnchor extends Anchor {
+
+               private final int markClass;                            // mark class
+
+               /**
+                * Instantiate a MarkAnchor
+                *
+                * @param markClass the mark class
+                * @param a         the underlying anchor (whose fields are copied)
+                */
+               public MarkAnchor(int markClass, Anchor a) {
+                       super(a);
+                       this.markClass = markClass;
+               }
+
+               /**
+                * @return the mark class
+                */
+               public int getMarkClass() {
+                       return markClass;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       return "{ markClass = " + markClass + ", anchor = " + super.toString() + " }";
+               }
+
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphProcessingState.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphProcessingState.java
new file mode 100644 (file)
index 0000000..6ad495f
--- /dev/null
@@ -0,0 +1,1359 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+import com.jaredrummler.fontreader.fonts.GlyphSubtable;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.GlyphTester;
+import com.jaredrummler.fontreader.util.ScriptContextTester;
+
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <p>The <code>GlyphProcessingState</code> implements a common, base state object used during glyph substitution
+ * and positioning processing.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GlyphProcessingState {
+
+       /**
+        * governing glyph definition table
+        */
+       protected GlyphDefinitionTable gdef;
+       /**
+        * governing script
+        */
+       protected String script;
+       /**
+        * governing language
+        */
+       protected String language;
+       /**
+        * governing feature
+        */
+       protected String feature;
+       /**
+        * current input glyph sequence
+        */
+       protected GlyphSequence igs;
+       /**
+        * current index in input sequence
+        */
+       protected int index;
+       /**
+        * last (maximum) index of input sequence (exclusive)
+        */
+       protected int indexLast;
+       /**
+        * consumed, updated after each successful subtable application
+        */
+       protected int consumed;
+       /**
+        * lookup flags
+        */
+       protected int lookupFlags;
+       /**
+        * class match set
+        */
+       protected int classMatchSet;
+       /**
+        * script specific context tester or null
+        */
+       protected ScriptContextTester sct;
+       /**
+        * glyph context tester or null
+        */
+       protected GlyphContextTester gct;
+       /**
+        * ignore base glyph tester
+        */
+       protected GlyphTester ignoreBase;
+       /**
+        * ignore ligature glyph tester
+        */
+       protected GlyphTester ignoreLigature;
+       /**
+        * ignore mark glyph tester
+        */
+       protected GlyphTester ignoreMark;
+       /**
+        * default ignore glyph tester
+        */
+       protected GlyphTester ignoreDefault;
+       /**
+        * current subtable
+        */
+       private GlyphSubtable subtable;
+
+       /**
+        * Construct default (reset) glyph processing state.
+        */
+       public GlyphProcessingState() {
+       }
+
+       /**
+        * Construct glyph processing state.
+        *
+        * @param gs       input glyph sequence
+        * @param script   script identifier
+        * @param language language identifier
+        * @param feature  feature identifier
+        * @param sct      script context tester (or null)
+        */
+       protected GlyphProcessingState(GlyphSequence gs, String script, String language, String feature,
+                                                                  ScriptContextTester sct) {
+               this.script = script;
+               this.language = language;
+               this.feature = feature;
+               this.igs = gs;
+               this.indexLast = gs.getGlyphCount();
+               this.sct = sct;
+               this.gct = (sct != null) ? sct.getTester(feature) : null;
+               this.ignoreBase = new GlyphTester() {
+
+                       public boolean test(int gi, int flags) {
+                               return isIgnoredBase(gi, flags);
+                       }
+               };
+               this.ignoreLigature = new GlyphTester() {
+
+                       public boolean test(int gi, int flags) {
+                               return isIgnoredLigature(gi, flags);
+                       }
+               };
+               this.ignoreMark = new GlyphTester() {
+
+                       public boolean test(int gi, int flags) {
+                               return isIgnoredMark(gi, flags);
+                       }
+               };
+       }
+
+       /**
+        * Construct glyph processing state using an existing state object using shallow copy
+        * except as follows: input glyph sequence is copied deep except for its characters array.
+        *
+        * @param s existing processing state to copy from
+        */
+       protected GlyphProcessingState(GlyphProcessingState s) {
+               this(new GlyphSequence(s.igs), s.script, s.language, s.feature, s.sct);
+               setPosition(s.index);
+       }
+
+       /**
+        * Reset glyph processing state.
+        *
+        * @param gs       input glyph sequence
+        * @param script   script identifier
+        * @param language language identifier
+        * @param feature  feature identifier
+        * @param sct      script context tester (or null)
+        * @return this instance
+        */
+       protected GlyphProcessingState reset(GlyphSequence gs, String script, String language, String feature,
+                                                                                ScriptContextTester sct) {
+               this.gdef = null;
+               this.script = script;
+               this.language = language;
+               this.feature = feature;
+               this.igs = gs;
+               this.index = 0;
+               this.indexLast = gs.getGlyphCount();
+               this.consumed = 0;
+               this.lookupFlags = 0;
+               this.classMatchSet = 0; // @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+               this.sct = sct;
+               this.gct = (sct != null) ? sct.getTester(feature) : null;
+               this.ignoreBase = new GlyphTester() {
+
+                       public boolean test(int gi, int flags) {
+                               return isIgnoredBase(gi, flags);
+                       }
+               };
+               this.ignoreLigature = new GlyphTester() {
+
+                       public boolean test(int gi, int flags) {
+                               return isIgnoredLigature(gi, flags);
+                       }
+               };
+               this.ignoreMark = new GlyphTester() {
+
+                       public boolean test(int gi, int flags) {
+                               return isIgnoredMark(gi, flags);
+                       }
+               };
+               this.ignoreDefault = null;
+               this.subtable = null;
+               return this;
+       }
+
+       /**
+        * Set governing glyph definition table.
+        *
+        * @param gdef glyph definition table (or null, to unset)
+        */
+       public void setGDEF(GlyphDefinitionTable gdef) {
+               if (this.gdef == null) {
+                       this.gdef = gdef;
+               } else if (gdef == null) {
+                       this.gdef = null;
+               }
+       }
+
+       /**
+        * Obtain governing glyph definition table.
+        *
+        * @return glyph definition table (or null, to not set)
+        */
+       public GlyphDefinitionTable getGDEF() {
+               return gdef;
+       }
+
+       /**
+        * Set governing lookup flags
+        *
+        * @param flags lookup flags (or zero, to unset)
+        */
+       public void setLookupFlags(int flags) {
+               if (this.lookupFlags == 0) {
+                       this.lookupFlags = flags;
+               } else if (flags == 0) {
+                       this.lookupFlags = 0;
+               }
+       }
+
+       /**
+        * Obtain governing lookup  flags.
+        *
+        * @return lookup flags (zero may indicate unset or no flags)
+        */
+       public int getLookupFlags() {
+               return lookupFlags;
+       }
+
+       /**
+        * Obtain governing class match set.
+        *
+        * @param gi glyph index that may be used to determine which match set applies
+        * @return class match set (zero may indicate unset or no set)
+        */
+       public int getClassMatchSet(int gi) {
+               return 0;
+       }
+
+       /**
+        * Set default ignore tester.
+        *
+        * @param ignoreDefault glyph tester (or null, to unset)
+        */
+       public void setIgnoreDefault(GlyphTester ignoreDefault) {
+               if (this.ignoreDefault == null) {
+                       this.ignoreDefault = ignoreDefault;
+               } else if (ignoreDefault == null) {
+                       this.ignoreDefault = null;
+               }
+       }
+
+       /**
+        * Obtain governing default ignores tester.
+        *
+        * @return default ignores tester
+        */
+       public GlyphTester getIgnoreDefault() {
+               return ignoreDefault;
+       }
+
+       /**
+        * Update glyph subtable specific state. Each time a
+        * different glyph subtable is to be applied, it is used
+        * to update this state prior to application, after which
+        * this state is to be reset.
+        *
+        * @param st glyph subtable to use for update
+        */
+       public void updateSubtableState(GlyphSubtable st) {
+               if (this.subtable != st) {
+                       setGDEF(st.getGDEF());
+                       setLookupFlags(st.getFlags());
+                       setIgnoreDefault(getIgnoreTester(getLookupFlags()));
+                       this.subtable = st;
+               }
+       }
+
+       /**
+        * Obtain current position index in input glyph sequence.
+        *
+        * @return current index
+        */
+       public int getPosition() {
+               return index;
+       }
+
+       /**
+        * Set (seek to) position index in input glyph sequence.
+        *
+        * @param index to seek to
+        * @throws IndexOutOfBoundsException if index is less than zero
+        *                                   or exceeds last valid position
+        */
+       public void setPosition(int index) throws IndexOutOfBoundsException {
+               if ((index >= 0) && (index <= indexLast)) {
+                       this.index = index;
+               } else {
+                       throw new IndexOutOfBoundsException();
+               }
+       }
+
+       /**
+        * Obtain last valid position index in input glyph sequence.
+        *
+        * @return current last index
+        */
+       public int getLastPosition() {
+               return indexLast;
+       }
+
+       /**
+        * Determine if at least one glyph remains in
+        * input sequence.
+        *
+        * @return true if one or more glyph remains
+        */
+       public boolean hasNext() {
+               return hasNext(1);
+       }
+
+       /**
+        * Determine if at least <code>count</code> glyphs remain in
+        * input sequence.
+        *
+        * @param count of glyphs to test
+        * @return true if at least <code>count</code> glyphs are available
+        */
+       public boolean hasNext(int count) {
+               return (index + count) <= indexLast;
+       }
+
+       /**
+        * Update the current position index based upon previously consumed
+        * glyphs, i.e., add the consuemd count to the current position index.
+        * If no glyphs were previously consumed, then forces exactly one
+        * glyph to be consumed.
+        *
+        * @return the new (updated) position index
+        */
+       public int next() {
+               if (index < indexLast) {
+                       // force consumption of at least one input glyph
+                       if (consumed == 0) {
+                               consumed = 1;
+                       }
+                       index += consumed;
+                       consumed = 0;
+                       if (index > indexLast) {
+                               index = indexLast;
+                       }
+               }
+               return index;
+       }
+
+       /**
+        * Determine if at least one backtrack (previous) glyph is present
+        * in input sequence.
+        *
+        * @return true if one or more glyph remains
+        */
+       public boolean hasPrev() {
+               return hasPrev(1);
+       }
+
+       /**
+        * Determine if at least <code>count</code> backtrack (previous) glyphs
+        * are present in input sequence.
+        *
+        * @param count of glyphs to test
+        * @return true if at least <code>count</code> glyphs are available
+        */
+       public boolean hasPrev(int count) {
+               return (index - count) >= 0;
+       }
+
+       /**
+        * Update the current position index based upon previously consumed
+        * glyphs, i.e., subtract the consuemd count from the current position index.
+        * If no glyphs were previously consumed, then forces exactly one
+        * glyph to be consumed. This method is used to traverse an input
+        * glyph sequence in reverse order.
+        *
+        * @return the new (updated) position index
+        */
+       public int prev() {
+               if (index > 0) {
+                       // force consumption of at least one input glyph
+                       if (consumed == 0) {
+                               consumed = 1;
+                       }
+                       index -= consumed;
+                       consumed = 0;
+                       if (index < 0) {
+                               index = 0;
+                       }
+               }
+               return index;
+       }
+
+       /**
+        * Record the consumption of <code>count</code> glyphs such that
+        * this consumption never exceeds the number of glyphs in the input glyph
+        * sequence.
+        *
+        * @param count of glyphs to consume
+        * @return newly adjusted consumption count
+        * @throws IndexOutOfBoundsException if count would cause consumption
+        *                                   to exceed count of glyphs in input glyph sequence
+        */
+       public int consume(int count) throws IndexOutOfBoundsException {
+               if ((consumed + count) <= indexLast) {
+                       consumed += count;
+                       return consumed;
+               } else {
+                       throw new IndexOutOfBoundsException();
+               }
+       }
+
+       /**
+        * Determine if any consumption has occurred.
+        *
+        * @return true if consumption count is greater than zero
+        */
+       public boolean didConsume() {
+               return consumed > 0;
+       }
+
+       /**
+        * Obtain reference to input glyph sequence, which must not be modified.
+        *
+        * @return input glyph sequence
+        */
+       public GlyphSequence getInput() {
+               return igs;
+       }
+
+       /**
+        * Obtain glyph at specified offset from current position.
+        *
+        * @param offset from current position
+        * @return glyph at specified offset from current position
+        * @throws IndexOutOfBoundsException if no glyph available at offset
+        */
+       public int getGlyph(int offset) throws IndexOutOfBoundsException {
+               int i = index + offset;
+               if ((i >= 0) && (i < indexLast)) {
+                       return igs.getGlyph(i);
+               } else {
+                       throw new IndexOutOfBoundsException("attempting index at " + i);
+               }
+       }
+
+       /**
+        * Obtain glyph at current position.
+        *
+        * @return glyph at current position
+        * @throws IndexOutOfBoundsException if no glyph available
+        */
+       public int getGlyph() throws IndexOutOfBoundsException {
+               return getGlyph(0);
+       }
+
+       /**
+        * Set (replace) glyph at specified offset from current position.
+        *
+        * @param offset from current position
+        * @param glyph  to set at specified offset from current position
+        * @throws IndexOutOfBoundsException if specified offset is not valid position
+        */
+       public void setGlyph(int offset, int glyph) throws IndexOutOfBoundsException {
+               int i = index + offset;
+               if ((i >= 0) && (i < indexLast)) {
+                       igs.setGlyph(i, glyph);
+               } else {
+                       throw new IndexOutOfBoundsException("attempting index at " + i);
+               }
+       }
+
+       /**
+        * Obtain character association of glyph at specified offset from current position.
+        *
+        * @param offset from current position
+        * @return character association of glyph at current position
+        * @throws IndexOutOfBoundsException if offset results in an invalid index into input glyph sequence
+        */
+       public CharAssociation getAssociation(int offset) throws IndexOutOfBoundsException {
+               int i = index + offset;
+               if ((i >= 0) && (i < indexLast)) {
+                       return igs.getAssociation(i);
+               } else {
+                       throw new IndexOutOfBoundsException("attempting index at " + i);
+               }
+       }
+
+       /**
+        * Obtain character association of glyph at current position.
+        *
+        * @return character association of glyph at current position
+        * @throws IndexOutOfBoundsException if no glyph available
+        */
+       public CharAssociation getAssociation() throws IndexOutOfBoundsException {
+               return getAssociation(0);
+       }
+
+       /**
+        * Obtain <code>count</code> glyphs starting at specified offset from current position. If
+        * <code>reverseOrder</code> is true, then glyphs are returned in reverse order starting at specified offset
+        * and going in reverse towards beginning of input glyph sequence.
+        *
+        * @param offset       from current position
+        * @param count        number of glyphs to obtain
+        * @param reverseOrder true if to obtain in reverse order
+        * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored)
+        * @param glyphs       array to use to fetch glyphs
+        * @param counts       int[2] array to receive fetched glyph counts, where counts[0] will
+        *                     receive the number of glyphs obtained, and counts[1] will receive the number of glyphs
+        *                     ignored
+        * @return array of glyphs
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public int[] getGlyphs(int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, int[] glyphs,
+                                                  int[] counts) throws IndexOutOfBoundsException {
+               if (count < 0) {
+                       count = getGlyphsAvailable(offset, reverseOrder, ignoreTester)[0];
+               }
+               int start = index + offset;
+               if (start < 0) {
+                       throw new IndexOutOfBoundsException("will attempt index at " + start);
+               } else if (!reverseOrder && ((start + count) > indexLast)) {
+                       throw new IndexOutOfBoundsException("will attempt index at " + (start + count));
+               } else if (reverseOrder && ((start + 1) < count)) {
+                       throw new IndexOutOfBoundsException("will attempt index at " + (start - count));
+               }
+               if (glyphs == null) {
+                       glyphs = new int[count];
+               } else if (glyphs.length != count) {
+                       throw new IllegalArgumentException(
+                                       "glyphs array is non-null, but its length (" + glyphs.length + "), is not equal to count (" + count + ")");
+               }
+               if (!reverseOrder) {
+                       return getGlyphsForward(start, count, ignoreTester, glyphs, counts);
+               } else {
+                       return getGlyphsReverse(start, count, ignoreTester, glyphs, counts);
+               }
+       }
+
+       private int[] getGlyphsForward(int start, int count, GlyphTester ignoreTester, int[] glyphs, int[] counts)
+                       throws IndexOutOfBoundsException {
+               int counted = 0;
+               int ignored = 0;
+               for (int i = start, n = indexLast; (i < n) && (counted < count); i++) {
+                       int gi = getGlyph(i - index);
+                       if (gi == 65535) {
+                               ignored++;
+                       } else {
+                               if ((ignoreTester == null) || !ignoreTester.test(gi, getLookupFlags())) {
+                                       glyphs[counted++] = gi;
+                               } else {
+                                       ignored++;
+                               }
+                       }
+               }
+               if ((counts != null) && (counts.length > 1)) {
+                       counts[0] = counted;
+                       counts[1] = ignored;
+               }
+               return glyphs;
+       }
+
+       private int[] getGlyphsReverse(int start, int count, GlyphTester ignoreTester, int[] glyphs, int[] counts)
+                       throws IndexOutOfBoundsException {
+               int counted = 0;
+               int ignored = 0;
+               for (int i = start; (i >= 0) && (counted < count); i--) {
+                       int gi = getGlyph(i - index);
+                       if (gi == 65535) {
+                               ignored++;
+                       } else {
+                               if ((ignoreTester == null) || !ignoreTester.test(gi, getLookupFlags())) {
+                                       glyphs[counted++] = gi;
+                               } else {
+                                       ignored++;
+                               }
+                       }
+               }
+               if ((counts != null) && (counts.length > 1)) {
+                       counts[0] = counted;
+                       counts[1] = ignored;
+               }
+               return glyphs;
+       }
+
+       /**
+        * Obtain <code>count</code> glyphs starting at specified offset from current position. If
+        * offset is negative, then glyphs are returned in reverse order starting at specified offset
+        * and going in reverse towards beginning of input glyph sequence.
+        *
+        * @param offset from current position
+        * @param count  number of glyphs to obtain
+        * @param glyphs array to use to fetch glyphs
+        * @param counts int[2] array to receive fetched glyph counts, where counts[0] will
+        *               receive the number of glyphs obtained, and counts[1] will receive the number of glyphs
+        *               ignored
+        * @return array of glyphs
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public int[] getGlyphs(int offset, int count, int[] glyphs, int[] counts) throws IndexOutOfBoundsException {
+               return getGlyphs(offset, count, offset < 0, ignoreDefault, glyphs, counts);
+       }
+
+       /**
+        * Obtain all glyphs starting from current position to end of input glyph sequence.
+        *
+        * @return array of available glyphs
+        * @throws IndexOutOfBoundsException if no glyph available
+        */
+       public int[] getGlyphs() throws IndexOutOfBoundsException {
+               return getGlyphs(0, indexLast - index, false, null, null, null);
+       }
+
+       /**
+        * Obtain <code>count</code> ignored glyphs starting at specified offset from current position. If
+        * <code>reverseOrder</code> is true, then glyphs are returned in reverse order starting at specified offset
+        * and going in reverse towards beginning of input glyph sequence.
+        *
+        * @param offset       from current position
+        * @param count        number of glyphs to obtain
+        * @param reverseOrder true if to obtain in reverse order
+        * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored)
+        * @param glyphs       array to use to fetch glyphs
+        * @param counts       int[2] array to receive fetched glyph counts, where counts[0] will
+        *                     receive the number of glyphs obtained, and counts[1] will receive the number of glyphs
+        *                     ignored
+        * @return array of glyphs
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public int[] getIgnoredGlyphs(int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, int[] glyphs,
+                                                                 int[] counts) throws IndexOutOfBoundsException {
+               return getGlyphs(offset, count, reverseOrder, new NotGlyphTester(ignoreTester), glyphs, counts);
+       }
+
+       /**
+        * Obtain <code>count</code> ignored glyphs starting at specified offset from current position. If
+        * <code>offset</code>
+        * is
+        * negative, then fetch in reverse order.
+        *
+        * @param offset from current position
+        * @param count  number of glyphs to obtain
+        * @return array of glyphs
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public int[] getIgnoredGlyphs(int offset, int count) throws IndexOutOfBoundsException {
+               return getIgnoredGlyphs(offset, count, offset < 0, ignoreDefault, null, null);
+       }
+
+       /**
+        * Determine if glyph at specified offset from current position is ignored. If <code>offset</code> is
+        * negative, then test in reverse order.
+        *
+        * @param offset       from current position
+        * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored)
+        * @return true if glyph is ignored
+        * @throws IndexOutOfBoundsException if offset results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public boolean isIgnoredGlyph(int offset, GlyphTester ignoreTester) throws IndexOutOfBoundsException {
+               return (ignoreTester != null) && ignoreTester.test(getGlyph(offset), getLookupFlags());
+       }
+
+       /**
+        * Determine if glyph at specified offset from current position is ignored. If <code>offset</code> is
+        * negative, then test in reverse order.
+        *
+        * @param offset from current position
+        * @return true if glyph is ignored
+        * @throws IndexOutOfBoundsException if offset results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public boolean isIgnoredGlyph(int offset) throws IndexOutOfBoundsException {
+               return isIgnoredGlyph(offset, ignoreDefault);
+       }
+
+       /**
+        * Determine if glyph at current position is ignored.
+        *
+        * @return true if glyph is ignored
+        * @throws IndexOutOfBoundsException if offset results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public boolean isIgnoredGlyph() throws IndexOutOfBoundsException {
+               return isIgnoredGlyph(getPosition());
+       }
+
+       /**
+        * Determine number of glyphs available starting at specified offset from current position. If
+        * <code>reverseOrder</code> is true, then search backwards in input glyph sequence.
+        *
+        * @param offset       from current position
+        * @param reverseOrder true if to obtain in reverse order
+        * @param ignoreTester glyph tester to use to determine which glyphs to count (or null, in which case none are ignored)
+        * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs
+        * ignored
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public int[] getGlyphsAvailable(int offset, boolean reverseOrder, GlyphTester ignoreTester)
+                       throws IndexOutOfBoundsException {
+               int start = index + offset;
+               if ((start < 0) || (start > indexLast)) {
+                       return new int[]{0, 0};
+               } else if (!reverseOrder) {
+                       return getGlyphsAvailableForward(start, ignoreTester);
+               } else {
+                       return getGlyphsAvailableReverse(start, ignoreTester);
+               }
+       }
+
+       private int[] getGlyphsAvailableForward(int start, GlyphTester ignoreTester) throws IndexOutOfBoundsException {
+               int counted = 0;
+               int ignored = 0;
+               if (ignoreTester == null) {
+                       counted = indexLast - start;
+               } else {
+                       for (int i = start, n = indexLast; i < n; i++) {
+                               int gi = getGlyph(i - index);
+                               if (gi == 65535) {
+                                       ignored++;
+                               } else {
+                                       if (ignoreTester.test(gi, getLookupFlags())) {
+                                               ignored++;
+                                       } else {
+                                               counted++;
+                                       }
+                               }
+                       }
+               }
+               return new int[]{counted, ignored};
+       }
+
+       private int[] getGlyphsAvailableReverse(int start, GlyphTester ignoreTester) throws IndexOutOfBoundsException {
+               int counted = 0;
+               int ignored = 0;
+               if (ignoreTester == null) {
+                       counted = start + 1;
+               } else {
+                       for (int i = start; i >= 0; i--) {
+                               int gi = getGlyph(i - index);
+                               if (gi == 65535) {
+                                       ignored++;
+                               } else {
+                                       if (ignoreTester.test(gi, getLookupFlags())) {
+                                               ignored++;
+                                       } else {
+                                               counted++;
+                                       }
+                               }
+                       }
+               }
+               return new int[]{counted, ignored};
+       }
+
+       /**
+        * Determine number of glyphs available starting at specified offset from current position. If
+        * <code>reverseOrder</code> is true, then search backwards in input glyph sequence. Uses the
+        * default ignores tester.
+        *
+        * @param offset       from current position
+        * @param reverseOrder true if to obtain in reverse order
+        * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs
+        * ignored
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public int[] getGlyphsAvailable(int offset, boolean reverseOrder) throws IndexOutOfBoundsException {
+               return getGlyphsAvailable(offset, reverseOrder, ignoreDefault);
+       }
+
+       /**
+        * Determine number of glyphs available starting at specified offset from current position. If
+        * offset is negative, then search backwards in input glyph sequence. Uses the
+        * default ignores tester.
+        *
+        * @param offset from current position
+        * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs
+        * ignored
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public int[] getGlyphsAvailable(int offset) throws IndexOutOfBoundsException {
+               return getGlyphsAvailable(offset, offset < 0);
+       }
+
+       /**
+        * Obtain <code>count</code> character associations of glyphs starting at specified offset from current position. If
+        * <code>reverseOrder</code> is true, then associations are returned in reverse order starting at specified offset
+        * and going in reverse towards beginning of input glyph sequence.
+        *
+        * @param offset       from current position
+        * @param count        number of associations to obtain
+        * @param reverseOrder true if to obtain in reverse order
+        * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored)
+        * @param associations array to use to fetch associations
+        * @param counts       int[2] array to receive fetched association counts, where counts[0] will
+        *                     receive the number of associations obtained, and counts[1] will receive the number of glyphs whose
+        *                     associations were ignored
+        * @return array of associations
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public CharAssociation[] getAssociations(int offset, int count, boolean reverseOrder, GlyphTester ignoreTester,
+                                                                                        CharAssociation[] associations, int[] counts)
+                       throws IndexOutOfBoundsException {
+               if (count < 0) {
+                       count = getGlyphsAvailable(offset, reverseOrder, ignoreTester)[0];
+               }
+               int start = index + offset;
+               if (start < 0) {
+                       throw new IndexOutOfBoundsException("will attempt index at " + start);
+               } else if (!reverseOrder && ((start + count) > indexLast)) {
+                       throw new IndexOutOfBoundsException("will attempt index at " + (start + count));
+               } else if (reverseOrder && ((start + 1) < count)) {
+                       throw new IndexOutOfBoundsException("will attempt index at " + (start - count));
+               }
+               if (associations == null) {
+                       associations = new CharAssociation[count];
+               } else if (associations.length != count) {
+                       throw new IllegalArgumentException(
+                                       "associations array is non-null, but its length (" + associations.length + "), is not equal to count (" +
+                                                       count + ")");
+               }
+               if (!reverseOrder) {
+                       return getAssociationsForward(start, count, ignoreTester, associations, counts);
+               } else {
+                       return getAssociationsReverse(start, count, ignoreTester, associations, counts);
+               }
+       }
+
+       private CharAssociation[] getAssociationsForward(int start, int count, GlyphTester ignoreTester,
+                                                                                                        CharAssociation[] associations, int[] counts)
+                       throws IndexOutOfBoundsException {
+               int counted = 0;
+               int ignored = 0;
+               for (int i = start, n = indexLast, k = 0; i < n; i++) {
+                       int gi = getGlyph(i - index);
+                       if (gi == 65535) {
+                               ignored++;
+                       } else {
+                               if ((ignoreTester == null) || !ignoreTester.test(gi, getLookupFlags())) {
+                                       if (k < count) {
+                                               associations[k++] = getAssociation(i - index);
+                                               counted++;
+                                       } else {
+                                               break;
+                                       }
+                               } else {
+                                       ignored++;
+                               }
+                       }
+               }
+               if ((counts != null) && (counts.length > 1)) {
+                       counts[0] = counted;
+                       counts[1] = ignored;
+               }
+               return associations;
+       }
+
+       private CharAssociation[] getAssociationsReverse(int start, int count, GlyphTester ignoreTester,
+                                                                                                        CharAssociation[] associations, int[] counts)
+                       throws IndexOutOfBoundsException {
+               int counted = 0;
+               int ignored = 0;
+               for (int i = start, k = 0; i >= 0; i--) {
+                       int gi = getGlyph(i - index);
+                       if (gi == 65535) {
+                               ignored++;
+                       } else {
+                               if ((ignoreTester == null) || !ignoreTester.test(gi, getLookupFlags())) {
+                                       if (k < count) {
+                                               associations[k++] = getAssociation(i - index);
+                                               counted++;
+                                       } else {
+                                               break;
+                                       }
+                               } else {
+                                       ignored++;
+                               }
+                       }
+               }
+               if ((counts != null) && (counts.length > 1)) {
+                       counts[0] = counted;
+                       counts[1] = ignored;
+               }
+               return associations;
+       }
+
+       /**
+        * Obtain <code>count</code> character associations of glyphs starting at specified offset from current position. If
+        * offset is negative, then search backwards in input glyph sequence. Uses the
+        * default ignores tester.
+        *
+        * @param offset from current position
+        * @param count  number of associations to obtain
+        * @return array of associations
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public CharAssociation[] getAssociations(int offset, int count) throws IndexOutOfBoundsException {
+               return getAssociations(offset, count, offset < 0, ignoreDefault, null, null);
+       }
+
+       /**
+        * Obtain <code>count</code> character associations of ignored glyphs starting at specified offset from current
+        * position. If
+        * <code>reverseOrder</code> is true, then glyphs are returned in reverse order starting at specified offset
+        * and going in reverse towards beginning of input glyph sequence.
+        *
+        * @param offset       from current position
+        * @param count        number of character associations to obtain
+        * @param reverseOrder true if to obtain in reverse order
+        * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored)
+        * @param associations array to use to fetch associations
+        * @param counts       int[2] array to receive fetched association counts, where counts[0] will
+        *                     receive the number of associations obtained, and counts[1] will receive the number of glyphs whose
+        *                     associations were ignored
+        * @return array of associations
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public CharAssociation[] getIgnoredAssociations(int offset, int count, boolean reverseOrder, GlyphTester ignoreTester,
+                                                                                                       CharAssociation[] associations, int[] counts)
+                       throws IndexOutOfBoundsException {
+               return getAssociations(offset, count, reverseOrder, new NotGlyphTester(ignoreTester), associations, counts);
+       }
+
+       /**
+        * Obtain <code>count</code> character associations of ignored glyphs starting at specified offset from current
+        * position. If
+        * offset is negative, then search backwards in input glyph sequence. Uses the
+        * default ignores tester.
+        *
+        * @param offset from current position
+        * @param count  number of character associations to obtain
+        * @return array of associations
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public CharAssociation[] getIgnoredAssociations(int offset, int count) throws IndexOutOfBoundsException {
+               return getIgnoredAssociations(offset, count, offset < 0, ignoreDefault, null, null);
+       }
+
+       /**
+        * Replace subsequence of input glyph sequence starting at specified offset from current position and of
+        * length <code>count</code> glyphs with a subsequence of the sequence <code>gs</code> starting from the specified
+        * offset <code>gsOffset</code> of length <code>gsCount</code> glyphs.
+        *
+        * @param offset   from current position
+        * @param count    number of glyphs to replace, which, if negative means all glyphs from offset to end of input sequence
+        * @param gs       glyph sequence from which to obtain replacement glyphs
+        * @param gsOffset offset of first glyph in replacement sequence
+        * @param gsCount  count of glyphs in replacement sequence starting at <code>gsOffset</code>
+        * @return true if replacement occurred, or false if replacement would result in no change to input glyph sequence
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public boolean replaceInput(int offset, int count, GlyphSequence gs, int gsOffset, int gsCount)
+                       throws IndexOutOfBoundsException {
+               int nig = (igs != null) ? igs.getGlyphCount() : 0;
+               int position = getPosition() + offset;
+               if (position < 0) {
+                       position = 0;
+               } else if (position > nig) {
+                       position = nig;
+               }
+               if ((count < 0) || ((position + count) > nig)) {
+                       count = nig - position;
+               }
+               int nrg = (gs != null) ? gs.getGlyphCount() : 0;
+               if (gsOffset < 0) {
+                       gsOffset = 0;
+               } else if (gsOffset > nrg) {
+                       gsOffset = nrg;
+               }
+               if ((gsCount < 0) || ((gsOffset + gsCount) > nrg)) {
+                       gsCount = nrg - gsOffset;
+               }
+               int ng = nig + gsCount - count;
+               IntBuffer gb = IntBuffer.allocate(ng);
+               List al = new ArrayList(ng);
+               for (int i = 0, n = position; i < n; i++) {
+                       gb.put(igs.getGlyph(i));
+                       al.add(igs.getAssociation(i));
+               }
+               for (int i = gsOffset, n = gsOffset + gsCount; i < n; i++) {
+                       gb.put(gs.getGlyph(i));
+                       al.add(gs.getAssociation(i));
+               }
+               for (int i = position + count, n = nig; i < n; i++) {
+                       gb.put(igs.getGlyph(i));
+                       al.add(igs.getAssociation(i));
+               }
+               gb.flip();
+               if (igs.compareGlyphs(gb) != 0) {
+                       this.igs = new GlyphSequence(igs.getCharacters(), gb, al);
+                       this.indexLast = gb.limit();
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Replace subsequence of input glyph sequence starting at specified offset from current position and of
+        * length <code>count</code> glyphs with all glyphs in the replacement sequence <code>gs</code>.
+        *
+        * @param offset from current position
+        * @param count  number of glyphs to replace, which, if negative means all glyphs from offset to end of input sequence
+        * @param gs     glyph sequence from which to obtain replacement glyphs
+        * @return true if replacement occurred, or false if replacement would result in no change to input glyph sequence
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public boolean replaceInput(int offset, int count, GlyphSequence gs) throws IndexOutOfBoundsException {
+               return replaceInput(offset, count, gs, 0, gs.getGlyphCount());
+       }
+
+       /**
+        * Erase glyphs in input glyph sequence starting at specified offset from current position, where each glyph
+        * in the specified <code>glyphs</code> array is matched, one at a time, and when a (forward searching) match is
+        * found
+        * in the input glyph sequence, the matching glyph is replaced with the glyph index 65535.
+        *
+        * @param offset from current position
+        * @param glyphs array of glyphs to erase
+        * @return the number of glyphs erased, which may be less than the number of specified glyphs
+        * @throws IndexOutOfBoundsException if offset or count results in an
+        *                                   invalid index into input glyph sequence
+        */
+       public int erase(int offset, int[] glyphs) throws IndexOutOfBoundsException {
+               int start = index + offset;
+               if ((start < 0) || (start > indexLast)) {
+                       throw new IndexOutOfBoundsException("will attempt index at " + start);
+               } else {
+                       int erased = 0;
+                       for (int i = start - index, n = indexLast - start; i < n; i++) {
+                               int gi = getGlyph(i);
+                               if (gi == glyphs[erased]) {
+                                       setGlyph(i, 65535);
+                                       erased++;
+                               }
+                       }
+                       return erased;
+               }
+       }
+
+       /**
+        * Determine if is possible that the current input sequence satisfies a script specific
+        * context testing predicate. If no predicate applies, then application is always possible.
+        *
+        * @return true if no script specific context tester applies or if a specified tester returns
+        * true for the current input sequence context
+        */
+       public boolean maybeApplicable() {
+               if (gct == null) {
+                       return true;
+               } else {
+                       return gct.test(script, language, feature, igs, index, getLookupFlags());
+               }
+       }
+
+       /**
+        * Apply default application semantices; namely, consume one glyph.
+        */
+       public void applyDefault() {
+               consumed += 1;
+       }
+
+       /**
+        * Determine if specified glyph is a base glyph according to the governing
+        * glyph definition table.
+        *
+        * @param gi glyph index to test
+        * @return true if glyph definition table records glyph as a base glyph; otherwise, false
+        */
+       public boolean isBase(int gi) {
+               if (gdef != null) {
+                       return gdef.isGlyphClass(gi, GlyphDefinitionTable.GLYPH_CLASS_BASE);
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Determine if specified glyph is an ignored base glyph according to the governing
+        * glyph definition table.
+        *
+        * @param gi    glyph index to test
+        * @param flags that apply to lookup in scope
+        * @return true if glyph definition table records glyph as a base glyph; otherwise, false
+        */
+       public boolean isIgnoredBase(int gi, int flags) {
+               return ((flags & GlyphSubtable.LF_IGNORE_BASE) != 0) && isBase(gi);
+       }
+
+       /**
+        * Determine if specified glyph is an ligature glyph according to the governing
+        * glyph definition table.
+        *
+        * @param gi glyph index to test
+        * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false
+        */
+       public boolean isLigature(int gi) {
+               if (gdef != null) {
+                       return gdef.isGlyphClass(gi, GlyphDefinitionTable.GLYPH_CLASS_LIGATURE);
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Determine if specified glyph is an ignored ligature glyph according to the governing
+        * glyph definition table.
+        *
+        * @param gi    glyph index to test
+        * @param flags that apply to lookup in scope
+        * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false
+        */
+       public boolean isIgnoredLigature(int gi, int flags) {
+               return ((flags & GlyphSubtable.LF_IGNORE_LIGATURE) != 0) && isLigature(gi);
+       }
+
+       /**
+        * Determine if specified glyph is a mark glyph according to the governing
+        * glyph definition table.
+        *
+        * @param gi glyph index to test
+        * @return true if glyph definition table records glyph as a mark glyph; otherwise, false
+        */
+       public boolean isMark(int gi) {
+               if (gdef != null) {
+                       return gdef.isGlyphClass(gi, GlyphDefinitionTable.GLYPH_CLASS_MARK);
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Determine if specified glyph is an ignored ligature glyph according to the governing
+        * glyph definition table.
+        *
+        * @param gi    glyph index to test
+        * @param flags that apply to lookup in scope
+        * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false
+        */
+       public boolean isIgnoredMark(int gi, int flags) {
+               if ((flags & GlyphSubtable.LF_IGNORE_MARK) != 0) {
+                       return isMark(gi);
+               } else if ((flags & GlyphSubtable.LF_MARK_ATTACHMENT_TYPE) != 0) {
+                       int lac = (flags & GlyphSubtable.LF_MARK_ATTACHMENT_TYPE) >> 8;
+                       int gac = gdef.getMarkAttachClass(gi);
+                       return (gac != lac);
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Obtain an ignored glyph tester that corresponds to the specified lookup flags.
+        *
+        * @param flags lookup flags
+        * @return a glyph tester
+        */
+       public GlyphTester getIgnoreTester(int flags) {
+               if ((flags & GlyphSubtable.LF_IGNORE_BASE) != 0) {
+                       if ((flags & (GlyphSubtable.LF_IGNORE_LIGATURE | GlyphSubtable.LF_IGNORE_MARK)) == 0) {
+                               return ignoreBase;
+                       } else {
+                               return getCombinedIgnoreTester(flags);
+                       }
+               }
+               if ((flags & GlyphSubtable.LF_IGNORE_LIGATURE) != 0) {
+                       if ((flags & (GlyphSubtable.LF_IGNORE_BASE | GlyphSubtable.LF_IGNORE_MARK)) == 0) {
+                               return ignoreLigature;
+                       } else {
+                               return getCombinedIgnoreTester(flags);
+                       }
+               }
+               if ((flags & GlyphSubtable.LF_IGNORE_MARK) != 0) {
+                       if ((flags & (GlyphSubtable.LF_IGNORE_BASE | GlyphSubtable.LF_IGNORE_LIGATURE)) == 0) {
+                               return ignoreMark;
+                       } else {
+                               return getCombinedIgnoreTester(flags);
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Obtain an ignored glyph tester that corresponds to the specified multiple (combined) lookup flags.
+        *
+        * @param flags lookup flags
+        * @return a glyph tester
+        */
+       public GlyphTester getCombinedIgnoreTester(int flags) {
+               GlyphTester[] gta = new GlyphTester[3];
+               int ngt = 0;
+               if ((flags & GlyphSubtable.LF_IGNORE_BASE) != 0) {
+                       gta[ngt++] = ignoreBase;
+               }
+               if ((flags & GlyphSubtable.LF_IGNORE_LIGATURE) != 0) {
+                       gta[ngt++] = ignoreLigature;
+               }
+               if ((flags & GlyphSubtable.LF_IGNORE_MARK) != 0) {
+                       gta[ngt++] = ignoreMark;
+               }
+               return getCombinedOrTester(gta, ngt);
+       }
+
+       /**
+        * Obtain an combined OR glyph tester.
+        *
+        * @param gta an array of glyph testers
+        * @param ngt number of glyph testers present in specified array
+        * @return a combined OR glyph tester
+        */
+       public GlyphTester getCombinedOrTester(GlyphTester[] gta, int ngt) {
+               if (ngt > 0) {
+                       return new CombinedOrGlyphTester(gta, ngt);
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Obtain an combined AND glyph tester.
+        *
+        * @param gta an array of glyph testers
+        * @param ngt number of glyph testers present in specified array
+        * @return a combined AND glyph tester
+        */
+       public GlyphTester getCombinedAndTester(GlyphTester[] gta, int ngt) {
+               if (ngt > 0) {
+                       return new CombinedAndGlyphTester(gta, ngt);
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * combined OR glyph tester
+        */
+       private static class CombinedOrGlyphTester implements GlyphTester {
+
+               private GlyphTester[] gta;
+               private int ngt;
+
+               CombinedOrGlyphTester(GlyphTester[] gta, int ngt) {
+                       this.gta = gta;
+                       this.ngt = ngt;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean test(int gi, int flags) {
+                       for (int i = 0, n = ngt; i < n; i++) {
+                               GlyphTester gt = gta[i];
+                               if (gt != null) {
+                                       if (gt.test(gi, flags)) {
+                                               return true;
+                                       }
+                               }
+                       }
+                       return false;
+               }
+       }
+
+       /**
+        * combined AND glyph tester
+        */
+       private static class CombinedAndGlyphTester implements GlyphTester {
+
+               private GlyphTester[] gta;
+               private int ngt;
+
+               CombinedAndGlyphTester(GlyphTester[] gta, int ngt) {
+                       this.gta = gta;
+                       this.ngt = ngt;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean test(int gi, int flags) {
+                       for (int i = 0, n = ngt; i < n; i++) {
+                               GlyphTester gt = gta[i];
+                               if (gt != null) {
+                                       if (!gt.test(gi, flags)) {
+                                               return false;
+                                       }
+                               }
+                       }
+                       return true;
+               }
+       }
+
+       /**
+        * NOT glyph tester
+        */
+       private static class NotGlyphTester implements GlyphTester {
+
+               private GlyphTester gt;
+
+               NotGlyphTester(GlyphTester gt) {
+                       this.gt = gt;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean test(int gi, int flags) {
+                       if (gt != null) {
+                               if (gt.test(gi, flags)) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Positionable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Positionable.java
new file mode 100644 (file)
index 0000000..c2d31d0
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+/**
+ * <p>Optional interface which indicates that glyph positioning is supported and, if supported,
+ * can perform positioning.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface Positionable {
+
+       /**
+        * Determines if font performs glyph positioning.
+        *
+        * @return true if performs positioning
+        */
+       boolean performsPositioning();
+
+       /**
+        * Perform glyph positioning.
+        *
+        * @param cs       character sequence to map to position offsets (advancement adjustments)
+        * @param script   a script identifier
+        * @param language a language identifier
+        * @param fontSize font size
+        * @return array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
+        * with one 4-tuple for each element of glyph sequence, or null if no non-zero adjustment applies
+        */
+       int[][] performPositioning(CharSequence cs, String script, String language, int fontSize);
+
+       /**
+        * Perform glyph positioning using an implied font size.
+        *
+        * @param cs       character sequence to map to position offsets (advancement adjustments)
+        * @param script   a script identifier
+        * @param language a language identifier
+        * @return array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
+        * with one 4-tuple for each element of glyph sequence, or null if no non-zero adjustment applies
+        */
+       int[][] performPositioning(CharSequence cs, String script, String language);
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Substitutable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Substitutable.java
new file mode 100644 (file)
index 0000000..3e43dfe
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.fonts;
+
+import java.util.List;
+
+/**
+ * <p>Optional interface which indicates that glyph substitution is supported and, if supported,
+ * can perform substitution.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface Substitutable {
+
+       /**
+        * Determines if font performs glyph substitution.
+        *
+        * @return true if performs substitution.
+        */
+       boolean performsSubstitution();
+
+       /**
+        * Perform substitutions on characters to effect glyph substitution. If some substitution is performed, it
+        * entails mapping from one or more input characters denoting textual character information to one or more
+        * output character codes denoting glyphs in this font, where the output character codes may make use of
+        * private character code values that have significance only for this font.
+        *
+        * @param cs             character sequence to map to output font encoding character sequence
+        * @param script         a script identifier
+        * @param language       a language identifier
+        * @param associations   optional list to receive list of character associations
+        * @param retainControls if true, then retain control characters and their glyph mappings, otherwise remove
+        * @return output sequence (represented as a character sequence, where each character in the returned sequence
+        * denotes "font characters", i.e., character codes that map directly (1-1) to their associated glyphs
+        */
+       CharSequence performSubstitution(CharSequence cs, String script, String language, List associations,
+                                                                        boolean retainControls);
+
+       /**
+        * Reorder combining marks in character sequence so that they precede (within the sequence) the base
+        * character to which they are applied. N.B. In the case of LTR segments, marks are not reordered by this,
+        * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede
+        * their base character.
+        *
+        * @param cs           character sequence within which combining marks to be reordered
+        * @param gpa          associated glyph position adjustments (also reordered)
+        * @param script       a script identifier
+        * @param language     a language identifier
+        * @param associations optional list of associations to be reordered
+        * @return output sequence containing reordered "font characters"
+        */
+       CharSequence reorderCombiningMarks(CharSequence cs, int[][] gpa, String script, String language, List associations);
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ArabicScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ArabicScriptProcessor.java
new file mode 100644 (file)
index 0000000..f17013c
--- /dev/null
@@ -0,0 +1,531 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.scripts;
+
+import com.jaredrummler.fontreader.complexscripts.bidi.BidiClass;
+import com.jaredrummler.fontreader.complexscripts.bidi.BidiConstants;
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphContextTester;
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphDefinitionTable;
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+import com.jaredrummler.fontreader.util.CharUtilities;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.ScriptContextTester;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <p>The <code>ArabicScriptProcessor</code> class implements a script processor for
+ * performing glyph substitution and positioning operations on content associated with the Arabic script.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class ArabicScriptProcessor extends DefaultScriptProcessor {
+
+       /**
+        * features to use for substitutions
+        */
+       private static final String[] GSUB_FEATURES = {
+                       "calt",                                                 // contextual alternates
+                       "ccmp",                                                 // glyph composition/decomposition
+                       "fina",                                                 // final (terminal) forms
+                       "init",                                                 // initial forms
+                       "isol",                                                 // isolated formas
+                       "liga",                                                 // standard ligatures
+                       "medi",                                                 // medial forms
+                       "rlig"                                                  // required ligatures
+       };
+
+       /**
+        * features to use for positioning
+        */
+       private static final String[] GPOS_FEATURES = {
+                       "curs",                                                 // cursive positioning
+                       "kern",                                                 // kerning
+                       "mark",                                                 // mark to base or ligature positioning
+                       "mkmk"                                                  // mark to mark positioning
+       };
+
+       private static class SubstitutionScriptContextTester implements ScriptContextTester {
+
+               private static Map/*<String,GlyphContextTester>*/ testerMap = new HashMap/*<String,GlyphContextTester>*/();
+
+               static {
+                       testerMap.put("fina", new GlyphContextTester() {
+
+                               public boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags) {
+                                       return inFinalContext(script, language, feature, gs, index, flags);
+                               }
+                       });
+                       testerMap.put("init", new GlyphContextTester() {
+
+                               public boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags) {
+                                       return inInitialContext(script, language, feature, gs, index, flags);
+                               }
+                       });
+                       testerMap.put("isol", new GlyphContextTester() {
+
+                               public boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags) {
+                                       return inIsolateContext(script, language, feature, gs, index, flags);
+                               }
+                       });
+                       testerMap.put("liga", new GlyphContextTester() {
+
+                               public boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags) {
+                                       return inLigatureContext(script, language, feature, gs, index, flags);
+                               }
+                       });
+                       testerMap.put("medi", new GlyphContextTester() {
+
+                               public boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags) {
+                                       return inMedialContext(script, language, feature, gs, index, flags);
+                               }
+                       });
+               }
+
+               public GlyphContextTester getTester(String feature) {
+                       return (GlyphContextTester) testerMap.get(feature);
+               }
+       }
+
+       private static class PositioningScriptContextTester implements ScriptContextTester {
+
+               private static Map/*<String,GlyphContextTester>*/ testerMap = new HashMap/*<String,GlyphContextTester>*/();
+
+               public GlyphContextTester getTester(String feature) {
+                       return (GlyphContextTester) testerMap.get(feature);
+               }
+       }
+
+       private final ScriptContextTester subContextTester;
+       private final ScriptContextTester posContextTester;
+
+       ArabicScriptProcessor(String script) {
+               super(script);
+               this.subContextTester = new SubstitutionScriptContextTester();
+               this.posContextTester = new PositioningScriptContextTester();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String[] getSubstitutionFeatures() {
+               return GSUB_FEATURES;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public ScriptContextTester getSubstitutionContextTester() {
+               return subContextTester;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String[] getPositioningFeatures() {
+               return GPOS_FEATURES;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public ScriptContextTester getPositioningContextTester() {
+               return posContextTester;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public GlyphSequence reorderCombiningMarks(GlyphDefinitionTable gdef, GlyphSequence gs, int[] widths, int[][] gpa,
+                                                                                          String script, String language) {
+               // a side effect of BIDI reordering is to order combining marks before their base, so we need to override the default here to
+               // prevent double reordering
+               return gs;
+       }
+
+       private static boolean inFinalContext(String script, String language, String feature, GlyphSequence gs, int index,
+                                                                                 int flags) {
+               CharAssociation a = gs.getAssociation(index);
+               int[] ca = gs.getCharacterArray(false);
+               int nc = gs.getCharacterCount();
+               if (nc == 0) {
+                       return false;
+               } else {
+                       int s = a.getStart();
+                       int e = a.getEnd();
+                       if (!hasFinalPrecedingContext(ca, nc, s, e)) {
+                               return false;
+                       } else if (!hasFinalThisContext(ca, nc, s, e)) {
+                               return false;
+                       } else if (forceFinalThisContext(ca, nc, s, e)) {
+                               return true;
+                       } else return hasFinalSucceedingContext(ca, nc, s, e);
+               }
+       }
+
+       private static boolean inInitialContext(String script, String language, String feature, GlyphSequence gs, int index,
+                                                                                       int flags) {
+               CharAssociation a = gs.getAssociation(index);
+               int[] ca = gs.getCharacterArray(false);
+               int nc = gs.getCharacterCount();
+               if (nc == 0) {
+                       return false;
+               } else {
+                       int s = a.getStart();
+                       int e = a.getEnd();
+                       if (!hasInitialPrecedingContext(ca, nc, s, e)) {
+                               return false;
+                       } else if (!hasInitialThisContext(ca, nc, s, e)) {
+                               return false;
+                       } else return hasInitialSucceedingContext(ca, nc, s, e);
+               }
+       }
+
+       private static boolean inIsolateContext(String script, String language, String feature, GlyphSequence gs, int index,
+                                                                                       int flags) {
+               CharAssociation a = gs.getAssociation(index);
+               int nc = gs.getCharacterCount();
+               if (nc == 0) {
+                       return false;
+               } else return (a.getStart() == 0) && (a.getEnd() == nc);
+       }
+
+       private static boolean inLigatureContext(String script, String language, String feature, GlyphSequence gs, int index,
+                                                                                        int flags) {
+               CharAssociation a = gs.getAssociation(index);
+               int[] ca = gs.getCharacterArray(false);
+               int nc = gs.getCharacterCount();
+               if (nc == 0) {
+                       return false;
+               } else {
+                       int s = a.getStart();
+                       int e = a.getEnd();
+                       if (!hasLigaturePrecedingContext(ca, nc, s, e)) {
+                               return false;
+                       } else return hasLigatureSucceedingContext(ca, nc, s, e);
+               }
+       }
+
+       private static boolean inMedialContext(String script, String language, String feature, GlyphSequence gs, int index,
+                                                                                  int flags) {
+               CharAssociation a = gs.getAssociation(index);
+               int[] ca = gs.getCharacterArray(false);
+               int nc = gs.getCharacterCount();
+               if (nc == 0) {
+                       return false;
+               } else {
+                       int s = a.getStart();
+                       int e = a.getEnd();
+                       if (!hasMedialPrecedingContext(ca, nc, s, e)) {
+                               return false;
+                       } else if (!hasMedialThisContext(ca, nc, s, e)) {
+                               return false;
+                       } else return hasMedialSucceedingContext(ca, nc, s, e);
+               }
+       }
+
+       private static boolean hasFinalPrecedingContext(int[] ca, int nc, int s, int e) {
+               int chp = 0;    // preceding non-NSM char in [0,s) searching back from s
+               int clp = 0;
+               for (int i = s; i > 0; i--) {
+                       int k = i - 1;
+                       if ((k >= 0) && (k < nc)) {
+                               chp = ca[k];
+                               clp = BidiClass.getBidiClass(chp);
+                               if (clp != BidiConstants.NSM) {
+                                       break;
+                               }
+                       }
+               }
+               if (clp != BidiConstants.AL) {
+                       return isZWJ(chp);
+               } else return !hasIsolateInitial(chp);
+       }
+
+       private static boolean hasFinalThisContext(int[] ca, int nc, int s, int e) {
+               int chl = 0;    // last non-{NSM,ZWJ} char in [s,e)
+               int cll = 0;
+               for (int i = 0, n = e - s; i < n; i++) {
+                       int k = n - i - 1;
+                       int j = s + k;
+                       if ((j >= 0) && (j < nc)) {
+                               chl = ca[j];
+                               cll = BidiClass.getBidiClass(chl);
+                               if ((cll != BidiConstants.NSM) && !isZWJ(chl)) {
+                                       break;
+                               }
+                       }
+               }
+               if (cll != BidiConstants.AL) {
+                       return false;
+               }
+               return !hasIsolateFinal(chl);
+       }
+
+       private static boolean forceFinalThisContext(int[] ca, int nc, int s, int e) {
+               int chl = 0;    // last non-{NSM,ZWJ} char in [s,e)
+               int cll = 0;
+               for (int i = 0, n = e - s; i < n; i++) {
+                       int k = n - i - 1;
+                       int j = s + k;
+                       if ((j >= 0) && (j < nc)) {
+                               chl = ca[j];
+                               cll = BidiClass.getBidiClass(chl);
+                               if ((cll != BidiConstants.NSM) && !isZWJ(chl)) {
+                                       break;
+                               }
+                       }
+               }
+               if (cll != BidiConstants.AL) {
+                       return false;
+               }
+               return hasIsolateInitial(chl);
+       }
+
+       private static boolean hasFinalSucceedingContext(int[] ca, int nc, int s, int e) {
+               int chs = 0;    // succeeding non-NSM char in [e,nc) searching forward from e
+               int cls = 0;
+               for (int i = e, n = nc; i < n; i++) {
+                       chs = ca[i];
+                       cls = BidiClass.getBidiClass(chs);
+                       if (cls != BidiConstants.NSM) {
+                               break;
+                       }
+               }
+               if (cls != BidiConstants.AL) {
+                       return !isZWJ(chs);
+               } else return hasIsolateFinal(chs);
+       }
+
+       private static boolean hasInitialPrecedingContext(int[] ca, int nc, int s, int e) {
+               int chp = 0;    // preceding non-NSM char in [0,s) searching back from s
+               int clp = 0;
+               for (int i = s; i > 0; i--) {
+                       int k = i - 1;
+                       if ((k >= 0) && (k < nc)) {
+                               chp = ca[k];
+                               clp = BidiClass.getBidiClass(chp);
+                               if (clp != BidiConstants.NSM) {
+                                       break;
+                               }
+                       }
+               }
+               if (clp != BidiConstants.AL) {
+                       return !isZWJ(chp);
+               } else return hasIsolateInitial(chp);
+       }
+
+       private static boolean hasInitialThisContext(int[] ca, int nc, int s, int e) {
+               int chf = 0;    // first non-{NSM,ZWJ} char in [s,e)
+               int clf = 0;
+               for (int i = 0, n = e - s; i < n; i++) {
+                       int k = s + i;
+                       if ((k >= 0) && (k < nc)) {
+                               chf = ca[s + i];
+                               clf = BidiClass.getBidiClass(chf);
+                               if ((clf != BidiConstants.NSM) && !isZWJ(chf)) {
+                                       break;
+                               }
+                       }
+               }
+               if (clf != BidiConstants.AL) {
+                       return false;
+               }
+               return !hasIsolateInitial(chf);
+       }
+
+       private static boolean hasInitialSucceedingContext(int[] ca, int nc, int s, int e) {
+               int chs = 0;    // succeeding non-NSM char in [e,nc) searching forward from e
+               int cls = 0;
+               for (int i = e, n = nc; i < n; i++) {
+                       chs = ca[i];
+                       cls = BidiClass.getBidiClass(chs);
+                       if (cls != BidiConstants.NSM) {
+                               break;
+                       }
+               }
+               if (cls != BidiConstants.AL) {
+                       return isZWJ(chs);
+               } else return !hasIsolateFinal(chs);
+       }
+
+       private static boolean hasMedialPrecedingContext(int[] ca, int nc, int s, int e) {
+               int chp = 0;    // preceding non-NSM char in [0,s) searching back from s
+               int clp = 0;
+               for (int i = s; i > 0; i--) {
+                       int k = i - 1;
+                       if ((k >= 0) && (k < nc)) {
+                               chp = ca[k];
+                               clp = BidiClass.getBidiClass(chp);
+                               if (clp != BidiConstants.NSM) {
+                                       break;
+                               }
+                       }
+               }
+               if (clp != BidiConstants.AL) {
+                       return isZWJ(chp);
+               } else return !hasIsolateInitial(chp);
+       }
+
+       private static boolean hasMedialThisContext(int[] ca, int nc, int s, int e) {
+               int chf = 0;    // first non-{NSM,ZWJ} char in [s,e)
+               int clf = 0;
+               for (int i = 0, n = e - s; i < n; i++) {
+                       int k = s + i;
+                       if ((k >= 0) && (k < nc)) {
+                               chf = ca[s + i];
+                               clf = BidiClass.getBidiClass(chf);
+                               if ((clf != BidiConstants.NSM) && !isZWJ(chf)) {
+                                       break;
+                               }
+                       }
+               }
+               if (clf != BidiConstants.AL) {
+                       return false;
+               }
+               int chl = 0;    // last non-{NSM,ZWJ} char in [s,e)
+               int cll = 0;
+               for (int i = 0, n = e - s; i < n; i++) {
+                       int k = n - i - 1;
+                       int j = s + k;
+                       if ((j >= 0) && (j < nc)) {
+                               chl = ca[j];
+                               cll = BidiClass.getBidiClass(chl);
+                               if ((cll != BidiConstants.NSM) && !isZWJ(chl)) {
+                                       break;
+                               }
+                       }
+               }
+               if (cll != BidiConstants.AL) {
+                       return false;
+               }
+               if (hasIsolateFinal(chf)) {
+                       return false;
+               } else return !hasIsolateInitial(chl);
+       }
+
+       private static boolean hasMedialSucceedingContext(int[] ca, int nc, int s, int e) {
+               int chs = 0;    // succeeding non-NSM char in [e,nc) searching forward from e
+               int cls = 0;
+               for (int i = e, n = nc; i < n; i++) {
+                       chs = ca[i];
+                       cls = BidiClass.getBidiClass(chs);
+                       if (cls != BidiConstants.NSM) {
+                               break;
+                       }
+               }
+               if (cls != BidiConstants.AL) {
+                       return isZWJ(chs);
+               } else return !hasIsolateFinal(chs);
+       }
+
+       private static boolean hasLigaturePrecedingContext(int[] ca, int nc, int s, int e) {
+               return true;
+       }
+
+       private static boolean hasLigatureSucceedingContext(int[] ca, int nc, int s, int e) {
+               int chs = 0;    // succeeding non-NSM char in [e,nc) searching forward from e
+               int cls = 0;
+               for (int i = e, n = nc; i < n; i++) {
+                       chs = ca[i];
+                       cls = BidiClass.getBidiClass(chs);
+                       // TBD - does ZWJ have impact here?
+                       if (cls != BidiConstants.NSM) {
+                               break;
+                       }
+               }
+               return cls == BidiConstants.AL;
+       }
+
+       /**
+        * Ordered array of Unicode scalars designating those Arabic (Script) Letters
+        * which exhibit an isolated form in word initial position.
+        */
+       private static final int[] ISOLATED_INITIALS = {
+                       0x0621, // HAMZA
+                       0x0622, // ALEF WITH MADDA ABOVE
+                       0x0623, // ALEF WITH HAMZA ABOVE
+                       0x0624, // WAW WITH HAMZA ABOVE
+                       0x0625, // ALEF WITH HAMZA BELOWW
+                       0x0627, // ALEF
+                       0x062F, // DAL
+                       0x0630, // THAL
+                       0x0631, // REH
+                       0x0632, // ZAIN
+                       0x0648, // WAW
+                       0x0671, // ALEF WASLA
+                       0x0672, // ALEF WITH WAVY HAMZA ABOVE
+                       0x0673, // ALEF WITH WAVY HAMZA BELOW
+                       0x0675, // HIGH HAMZA ALEF
+                       0x0676, // HIGH HAMZA WAW
+                       0x0677, // U WITH HAMZA ABOVE
+                       0x0688, // DDAL
+                       0x0689, // DAL WITH RING
+                       0x068A, // DAL WITH DOT BELOW
+                       0x068B, // DAL WITH DOT BELOW AND SMALL TAH
+                       0x068C, // DAHAL
+                       0x068D, // DDAHAL
+                       0x068E, // DUL
+                       0x068F, // DUL WITH THREE DOTS ABOVE DOWNWARDS
+                       0x0690, // DUL WITH FOUR DOTS ABOVE
+                       0x0691, // RREH
+                       0x0692, // REH WITH SMALL V
+                       0x0693, // REH WITH RING
+                       0x0694, // REH WITH DOT BELOW
+                       0x0695, // REH WITH SMALL V BELOW
+                       0x0696, // REH WITH DOT BELOW AND DOT ABOVE
+                       0x0697, // REH WITH TWO DOTS ABOVE
+                       0x0698, // JEH
+                       0x0699, // REH WITH FOUR DOTS ABOVE
+                       0x06C4, // WAW WITH RING
+                       0x06C5, // KIRGHIZ OE
+                       0x06C6, // OE
+                       0x06C7, // U
+                       0x06C8, // YU
+                       0x06C9, // KIRGHIZ YU
+                       0x06CA, // WAW WITH TWO DOTS ABOVE
+                       0x06CB, // VE
+                       0x06CF, // WAW WITH DOT ABOVE
+                       0x06EE, // DAL WITH INVERTED V
+                       0x06EF  // REH WITH INVERTED V
+       };
+
+       private static boolean hasIsolateInitial(int ch) {
+               return Arrays.binarySearch(ISOLATED_INITIALS, ch) >= 0;
+       }
+
+       /**
+        * Ordered array of Unicode scalars designating those Arabic (Script) Letters
+        * which exhibit an isolated form in word final position.
+        */
+       private static final int[] ISOLATED_FINALS = {
+                       0x0621  // HAMZA
+       };
+
+       private static boolean hasIsolateFinal(int ch) {
+               return Arrays.binarySearch(ISOLATED_FINALS, ch) >= 0;
+       }
+
+       private static boolean isZWJ(int ch) {
+               return ch == CharUtilities.ZERO_WIDTH_JOINER;
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DefaultScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DefaultScriptProcessor.java
new file mode 100644 (file)
index 0000000..d94fbde
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.scripts;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphDefinitionTable;
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.ScriptContextTester;
+
+/**
+ * <p>Default script processor, which enables default glyph composition/decomposition, common ligatures, localized
+ * forms
+ * and kerning.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class DefaultScriptProcessor extends ScriptProcessor {
+
+       /**
+        * features to use for substitutions
+        */
+       private static final String[] GSUB_FEATURES = {
+                       "ccmp",                                                 // glyph composition/decomposition
+                       "liga",                                                 // common ligatures
+                       "locl"                                                  // localized forms
+       };
+
+       /**
+        * features to use for positioning
+        */
+       private static final String[] GPOS_FEATURES = {
+                       "kern",                                                 // kerning
+                       "mark",                                                 // mark to base or ligature positioning
+                       "mkmk"                                                  // mark to mark positioning
+       };
+
+       DefaultScriptProcessor(String script) {
+               super(script);
+       }
+
+       @Override
+       /** {@inheritDoc} */
+       public String[] getSubstitutionFeatures() {
+               return GSUB_FEATURES;
+       }
+
+       @Override
+       /** {@inheritDoc} */
+       public ScriptContextTester getSubstitutionContextTester() {
+               return null;
+       }
+
+       @Override
+       /** {@inheritDoc} */
+       public String[] getPositioningFeatures() {
+               return GPOS_FEATURES;
+       }
+
+       @Override
+       /** {@inheritDoc} */
+       public ScriptContextTester getPositioningContextTester() {
+               return null;
+       }
+
+       @Override
+       /** {@inheritDoc} */
+       public GlyphSequence reorderCombiningMarks(GlyphDefinitionTable gdef, GlyphSequence gs, int[] unscaledWidths,
+                                                                                          int[][] gpa, String script, String language) {
+               int ng = gs.getGlyphCount();
+               int[] ga = gs.getGlyphArray(false);
+               int nm = 0;
+               // count combining marks
+               for (int i = 0; i < ng; i++) {
+                       int gid = ga[i];
+                       int gw = unscaledWidths[i];
+                       if (isReorderedMark(gdef, ga, unscaledWidths, i)) {
+                               nm++;
+                       }
+               }
+               // only reorder if there is at least one mark and at least one non-mark glyph
+               if ((nm > 0) && ((ng - nm) > 0)) {
+                       CharAssociation[] aa = gs.getAssociations(0, -1);
+                       int[] nga = new int[ng];
+                       int[][] npa = (gpa != null) ? new int[ng][] : null;
+                       CharAssociation[] naa = new CharAssociation[ng];
+                       int k = 0;
+                       CharAssociation ba = null;
+                       int bg = -1;
+                       int[] bpa = null;
+                       for (int i = 0; i < ng; i++) {
+                               int gid = ga[i];
+                               int[] pa = (gpa != null) ? gpa[i] : null;
+                               CharAssociation ca = aa[i];
+                               if (isReorderedMark(gdef, ga, unscaledWidths, i)) {
+                                       nga[k] = gid;
+                                       naa[k] = ca;
+                                       if (npa != null) {
+                                               npa[k] = pa;
+                                       }
+                                       k++;
+                               } else {
+                                       if (bg != -1) {
+                                               nga[k] = bg;
+                                               naa[k] = ba;
+                                               if (npa != null) {
+                                                       npa[k] = bpa;
+                                               }
+                                               k++;
+                                               bg = -1;
+                                               ba = null;
+                                               bpa = null;
+                                       }
+                                       if (bg == -1) {
+                                               bg = gid;
+                                               ba = ca;
+                                               bpa = pa;
+                                       }
+                               }
+                       }
+                       if (bg != -1) {
+                               nga[k] = bg;
+                               naa[k] = ba;
+                               if (npa != null) {
+                                       npa[k] = bpa;
+                               }
+                               k++;
+                       }
+                       assert k == ng;
+                       if (npa != null) {
+                               System.arraycopy(npa, 0, gpa, 0, ng);
+                       }
+                       return new GlyphSequence(gs, null, nga, null, null, naa, null);
+               } else {
+                       return gs;
+               }
+       }
+
+       protected boolean isReorderedMark(GlyphDefinitionTable gdef, int[] glyphs, int[] unscaledWidths, int index) {
+               return gdef.isGlyphClass(glyphs[index], GlyphDefinitionTable.GLYPH_CLASS_MARK) && (unscaledWidths[index] != 0);
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DevanagariScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DevanagariScriptProcessor.java
new file mode 100644 (file)
index 0000000..023a1b7
--- /dev/null
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.scripts;
+
+// CSOFF: LineLengthCheck
+
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+
+/**
+ * <p>The <code>DevanagariScriptProcessor</code> class implements a script processor for
+ * performing glyph substitution and positioning operations on content associated with the Devanagari script.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class DevanagariScriptProcessor extends IndicScriptProcessor {
+
+       DevanagariScriptProcessor(String script) {
+               super(script);
+       }
+
+       @Override
+       protected Class<? extends DevanagariSyllabizer> getSyllabizerClass() {
+               return DevanagariSyllabizer.class;
+       }
+
+       @Override
+       // find rightmost pre-base matra
+       protected int findPreBaseMatra(GlyphSequence gs) {
+               int ng = gs.getGlyphCount();
+               int lk = -1;
+               for (int i = ng; i > 0; i--) {
+                       int k = i - 1;
+                       if (containsPreBaseMatra(gs, k)) {
+                               lk = k;
+                               break;
+                       }
+               }
+               return lk;
+       }
+
+       @Override
+       // find leftmost pre-base matra target, starting from source
+       protected int findPreBaseMatraTarget(GlyphSequence gs, int source) {
+               int ng = gs.getGlyphCount();
+               int lk = -1;
+               for (int i = (source < ng) ? source : ng; i > 0; i--) {
+                       int k = i - 1;
+                       if (containsConsonant(gs, k)) {
+                               if (containsHalfConsonant(gs, k)) {
+                                       lk = k;
+                               } else if (lk == -1) {
+                                       lk = k;
+                               } else {
+                                       break;
+                               }
+                       }
+               }
+               return lk;
+       }
+
+       private static boolean containsPreBaseMatra(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isPreM(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsConsonant(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isC(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsHalfConsonant(GlyphSequence gs, int k) {
+               Boolean half = (Boolean) gs.getAssociation(k).getPredication("half");
+               return (half != null) ? half.booleanValue() : false;
+       }
+
+       @Override
+       protected int findReph(GlyphSequence gs) {
+               int ng = gs.getGlyphCount();
+               int li = -1;
+               for (int i = 0; i < ng; i++) {
+                       if (containsReph(gs, i)) {
+                               li = i;
+                               break;
+                       }
+               }
+               return li;
+       }
+
+       @Override
+       protected int findRephTarget(GlyphSequence gs, int source) {
+               int ng = gs.getGlyphCount();
+               int c1 = -1;
+               int c2 = -1;
+               // first candidate target is after first non-half consonant
+               for (int i = 0; i < ng; i++) {
+                       if ((i != source) && containsConsonant(gs, i)) {
+                               if (!containsHalfConsonant(gs, i)) {
+                                       c1 = i + 1;
+                                       break;
+                               }
+                       }
+               }
+               // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark
+               for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) {
+                       if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) {
+                               c2 = i + 1;
+                       } else if (containsOtherMark(gs, i)) {
+                               c2 = i;
+                               break;
+                       }
+               }
+               if (c2 >= 0) {
+                       return c2;
+               } else if (c1 >= 0) {
+                       return c1;
+               } else {
+                       return source;
+               }
+       }
+
+       private static boolean containsReph(GlyphSequence gs, int k) {
+               Boolean rphf = (Boolean) gs.getAssociation(k).getPredication("rphf");
+               return (rphf != null) ? rphf.booleanValue() : false;
+       }
+
+       private static boolean containsMatra(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isM(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsOtherMark(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       switch (typeOf(ca[i])) {
+                               case C_T:   // tone (e.g., udatta, anudatta)
+                               case C_A:   // accent (e.g., acute, grave)
+                               case C_O:   // other (e.g., candrabindu, anusvara, visarga, etc)
+                                       return true;
+                               default:
+                                       break;
+                       }
+               }
+               return false;
+       }
+
+       private static class DevanagariSyllabizer extends DefaultSyllabizer {
+
+               DevanagariSyllabizer(String script, String language) {
+                       super(script, language);
+               }
+
+               @Override
+               // | C ...
+               protected int findStartOfSyllable(int[] ca, int s, int e) {
+                       if ((s < 0) || (s >= e)) {
+                               return -1;
+                       } else {
+                               while (s < e) {
+                                       int c = ca[s];
+                                       if (isC(c)) {
+                                               break;
+                                       } else {
+                                               s++;
+                                       }
+                               }
+                               return s;
+                       }
+               }
+
+               @Override
+               // D* L? | ...
+               protected int findEndOfSyllable(int[] ca, int s, int e) {
+                       if ((s < 0) || (s >= e)) {
+                               return -1;
+                       } else {
+                               int nd = 0;
+                               int nl = 0;
+                               int i;
+                               // consume dead consonants
+                               while ((i = isDeadConsonant(ca, s, e)) > s) {
+                                       s = i;
+                                       nd++;
+                               }
+                               // consume zero or one live consonant
+                               if ((i = isLiveConsonant(ca, s, e)) > s) {
+                                       s = i;
+                                       nl++;
+                               }
+                               return ((nd > 0) || (nl > 0)) ? s : -1;
+                       }
+               }
+
+               // D := ( C N? H )?
+               private int isDeadConsonant(int[] ca, int s, int e) {
+                       if (s < 0) {
+                               return -1;
+                       } else {
+                               int c;
+                               int i = 0;
+                               int nc = 0;
+                               int nh = 0;
+                               do {
+                                       // C
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isC(c)) {
+                                                       i++;
+                                                       nc++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                                       // N?
+                                       if ((s + i) < e) {
+                                               c = ca[s + 1];
+                                               if (isN(c)) {
+                                                       i++;
+                                               }
+                                       }
+                                       // H
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isH(c)) {
+                                                       i++;
+                                                       nh++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                               } while (false);
+                               return (nc > 0) && (nh > 0) ? s + i : -1;
+                       }
+               }
+
+               // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK )
+               private int isLiveConsonant(int[] ca, int s, int e) {
+                       if (s < 0) {
+                               return -1;
+                       } else {
+                               int c;
+                               int i = 0;
+                               int nc = 0;
+                               int nv = 0;
+                               int nx = 0;
+                               do {
+                                       // C
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isC(c)) {
+                                                       i++;
+                                                       nc++;
+                                               } else if (isV(c)) {
+                                                       i++;
+                                                       nv++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                                       // N?
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isN(c)) {
+                                                       i++;
+                                               }
+                                       }
+                                       // X*
+                                       while ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isX(c)) {
+                                                       i++;
+                                                       nx++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                               } while (false);
+                               // if no X but has H, then ignore C|I
+                               if (nx == 0) {
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isH(c)) {
+                                                       if (nc > 0) {
+                                                               nc--;
+                                                       } else if (nv > 0) {
+                                                               nv--;
+                                                       }
+                                               }
+                                       }
+                               }
+                               return ((nc > 0) || (nv > 0)) ? s + i : -1;
+                       }
+               }
+       }
+
+       // devanagari character types
+       static final short C_U = 0;            // unassigned
+       static final short C_C = 1;            // consonant
+       static final short C_V = 2;            // vowel
+       static final short C_M = 3;            // vowel sign (matra)
+       static final short C_S = 4;            // symbol or sign
+       static final short C_T = 5;            // tone mark
+       static final short C_A = 6;            // accent mark
+       static final short C_P = 7;            // punctuation
+       static final short C_D = 8;            // digit
+       static final short C_H = 9;            // halant (virama)
+       static final short C_O = 10;           // other signs
+       static final short C_N = 0x0100;       // nukta(ized)
+       static final short C_R = 0x0200;       // reph(ized)
+       static final short C_PRE = 0x0400;       // pre-base
+       static final short C_M_TYPE = 0x00FF;       // type mask
+       static final short C_M_FLAGS = 0x7F00;       // flag mask
+       // devanagari block range
+       static final int CCA_START = 0x0900;      // first code point mapped by cca
+       static final int CCA_END = 0x0980;      // last code point + 1 mapped by cca
+       // devanagari character type lookups
+       static final short[] CCA = {
+                       C_O,                        // 0x0900       // INVERTED CANDRABINDU
+                       C_O,                        // 0x0901       // CANDRABINDU
+                       C_O,                        // 0x0902       // ANUSVARA
+                       C_O,                        // 0x0903       // VISARGA
+                       C_V,                        // 0x0904       // SHORT A
+                       C_V,                        // 0x0905       // A
+                       C_V,                        // 0x0906       // AA
+                       C_V,                        // 0x0907       // I
+                       C_V,                        // 0x0908       // II
+                       C_V,                        // 0x0909       // U
+                       C_V,                        // 0x090A       // UU
+                       C_V,                        // 0x090B       // VOCALIC R
+                       C_V,                        // 0x090C       // VOCALIC L
+                       C_V,                        // 0x090D       // CANDRA E
+                       C_V,                        // 0x090E       // SHORT E
+                       C_V,                        // 0x090F       // E
+                       C_V,                        // 0x0910       // AI
+                       C_V,                        // 0x0911       // CANDRA O
+                       C_V,                        // 0x0912       // SHORT O
+                       C_V,                        // 0x0913       // O
+                       C_V,                        // 0x0914       // AU
+                       C_C,                        // 0x0915       // KA
+                       C_C,                        // 0x0916       // KHA
+                       C_C,                        // 0x0917       // GA
+                       C_C,                        // 0x0918       // GHA
+                       C_C,                        // 0x0919       // NGA
+                       C_C,                        // 0x091A       // CA
+                       C_C,                        // 0x091B       // CHA
+                       C_C,                        // 0x091C       // JA
+                       C_C,                        // 0x091D       // JHA
+                       C_C,                        // 0x091E       // NYA
+                       C_C,                        // 0x091F       // TTA
+                       C_C,                        // 0x0920       // TTHA
+                       C_C,                        // 0x0921       // DDA
+                       C_C,                        // 0x0922       // DDHA
+                       C_C,                        // 0x0923       // NNA
+                       C_C,                        // 0x0924       // TA
+                       C_C,                        // 0x0925       // THA
+                       C_C,                        // 0x0926       // DA
+                       C_C,                        // 0x0927       // DHA
+                       C_C,                        // 0x0928       // NA
+                       C_C,                        // 0x0929       // NNNA
+                       C_C,                        // 0x092A       // PA
+                       C_C,                        // 0x092B       // PHA
+                       C_C,                        // 0x092C       // BA
+                       C_C,                        // 0x092D       // BHA
+                       C_C,                        // 0x092E       // MA
+                       C_C,                        // 0x092F       // YA
+                       C_C | C_R,                  // 0x0930       // RA
+                       C_C | C_R | C_N,            // 0x0931       // RRA          = 0930+093C
+                       C_C,                        // 0x0932       // LA
+                       C_C,                        // 0x0933       // LLA
+                       C_C,                        // 0x0934       // LLLA
+                       C_C,                        // 0x0935       // VA
+                       C_C,                        // 0x0936       // SHA
+                       C_C,                        // 0x0937       // SSA
+                       C_C,                        // 0x0938       // SA
+                       C_C,                        // 0x0939       // HA
+                       C_M,                        // 0x093A       // OE (KASHMIRI)
+                       C_M,                        // 0x093B       // OOE (KASHMIRI)
+                       C_N,                        // 0x093C       // NUKTA
+                       C_S,                        // 0x093D       // AVAGRAHA
+                       C_M,                        // 0x093E       // AA
+                       C_M | C_PRE,                // 0x093F       // I
+                       C_M,                        // 0x0940       // II
+                       C_M,                        // 0x0941       // U
+                       C_M,                        // 0x0942       // UU
+                       C_M,                        // 0x0943       // VOCALIC R
+                       C_M,                        // 0x0944       // VOCALIC RR
+                       C_M,                        // 0x0945       // CANDRA E
+                       C_M,                        // 0x0946       // SHORT E
+                       C_M,                        // 0x0947       // E
+                       C_M,                        // 0x0948       // AI
+                       C_M,                        // 0x0949       // CANDRA O
+                       C_M,                        // 0x094A       // SHORT O
+                       C_M,                        // 0x094B       // O
+                       C_M,                        // 0x094C       // AU
+                       C_H,                        // 0x094D       // VIRAMA (HALANT)
+                       C_M,                        // 0x094E       // PRISHTHAMATRA E
+                       C_M,                        // 0x094F       // AW
+                       C_S,                        // 0x0950       // OM
+                       C_T,                        // 0x0951       // UDATTA
+                       C_T,                        // 0x0952       // ANUDATTA
+                       C_A,                        // 0x0953       // GRAVE
+                       C_A,                        // 0x0954       // ACUTE
+                       C_M,                        // 0x0955       // CANDRA LONG E
+                       C_M,                        // 0x0956       // UE
+                       C_M,                        // 0x0957       // UUE
+                       C_C | C_N,                  // 0x0958       // QA
+                       C_C | C_N,                  // 0x0959       // KHHA
+                       C_C | C_N,                  // 0x095A       // GHHA
+                       C_C | C_N,                  // 0x095B       // ZA
+                       C_C | C_N,                  // 0x095C       // DDDHA
+                       C_C | C_N,                  // 0x095D       // RHA
+                       C_C | C_N,                  // 0x095E       // FA
+                       C_C | C_N,                  // 0x095F       // YYA
+                       C_V,                        // 0x0960       // VOCALIC RR
+                       C_V,                        // 0x0961       // VOCALIC LL
+                       C_M,                        // 0x0962       // VOCALIC RR
+                       C_M,                        // 0x0963       // VOCALIC LL
+                       C_P,                        // 0x0964       // DANDA
+                       C_P,                        // 0x0965       // DOUBLE DANDA
+                       C_D,                        // 0x0966       // ZERO
+                       C_D,                        // 0x0967       // ONE
+                       C_D,                        // 0x0968       // TWO
+                       C_D,                        // 0x0969       // THREE
+                       C_D,                        // 0x096A       // FOUR
+                       C_D,                        // 0x096B       // FIVE
+                       C_D,                        // 0x096C       // SIX
+                       C_D,                        // 0x096D       // SEVEN
+                       C_D,                        // 0x096E       // EIGHT
+                       C_D,                        // 0x096F       // NINE
+                       C_S,                        // 0x0970       // ABBREVIATION SIGN
+                       C_S,                        // 0x0971       // HIGH SPACING DOT
+                       C_V,                        // 0x0972       // CANDRA A (MARATHI)
+                       C_V,                        // 0x0973       // OE (KASHMIRI)
+                       C_V,                        // 0x0974       // OOE (KASHMIRI)
+                       C_V,                        // 0x0975       // AW (KASHMIRI)
+                       C_V,                        // 0x0976       // UE (KASHMIRI)
+                       C_V,                        // 0x0977       // UUE (KASHMIRI)
+                       C_U,                        // 0x0978       // UNASSIGNED
+                       C_C,                        // 0x0979       // ZHA
+                       C_C,                        // 0x097A       // HEAVY YA
+                       C_C,                        // 0x097B       // GGAA (SINDHI)
+                       C_C,                        // 0x097C       // JJA (SINDHI)
+                       C_C,                        // 0x097D       // GLOTTAL STOP (LIMBU)
+                       C_C,                        // 0x097E       // DDDA (SINDHI)
+                       C_C                         // 0x097F       // BBA (SINDHI)
+       };
+
+       static int typeOf(int c) {
+               if ((c >= CCA_START) && (c < CCA_END)) {
+                       return CCA[c - CCA_START] & C_M_TYPE;
+               } else {
+                       return C_U;
+               }
+       }
+
+       static boolean isType(int c, int t) {
+               return typeOf(c) == t;
+       }
+
+       static boolean hasFlag(int c, int f) {
+               if ((c >= CCA_START) && (c < CCA_END)) {
+                       return (CCA[c - CCA_START] & f) == f;
+               } else {
+                       return false;
+               }
+       }
+
+       static boolean isC(int c) {
+               return isType(c, C_C);
+       }
+
+       static boolean isR(int c) {
+               return isType(c, C_C) && hasR(c);
+       }
+
+       static boolean isV(int c) {
+               return isType(c, C_V);
+       }
+
+       static boolean isN(int c) {
+               return c == 0x093C;
+       }
+
+       static boolean isH(int c) {
+               return c == 0x094D;
+       }
+
+       static boolean isM(int c) {
+               return isType(c, C_M);
+       }
+
+       static boolean isPreM(int c) {
+               return isType(c, C_M) && hasFlag(c, C_PRE);
+       }
+
+       static boolean isX(int c) {
+               switch (typeOf(c)) {
+                       case C_M: // matra (combining vowel)
+                       case C_A: // accent mark
+                       case C_T: // tone mark
+                       case C_O: // other (modifying) mark
+                               return true;
+                       default:
+                               return false;
+               }
+       }
+
+       static boolean hasR(int c) {
+               return hasFlag(c, C_R);
+       }
+
+       static boolean hasN(int c) {
+               return hasFlag(c, C_N);
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GujaratiScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GujaratiScriptProcessor.java
new file mode 100644 (file)
index 0000000..a524596
--- /dev/null
@@ -0,0 +1,548 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.scripts;
+
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+
+/**
+ * <p>The <code>GujaratiScriptProcessor</code> class implements a script processor for
+ * performing glyph substitution and positioning operations on content associated with the Gujarati script.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GujaratiScriptProcessor extends IndicScriptProcessor {
+
+       GujaratiScriptProcessor(String script) {
+               super(script);
+       }
+
+       @Override
+       protected Class<? extends GujaratiSyllabizer> getSyllabizerClass() {
+               return GujaratiSyllabizer.class;
+       }
+
+       @Override
+       // find rightmost pre-base matra
+       protected int findPreBaseMatra(GlyphSequence gs) {
+               int ng = gs.getGlyphCount();
+               int lk = -1;
+               for (int i = ng; i > 0; i--) {
+                       int k = i - 1;
+                       if (containsPreBaseMatra(gs, k)) {
+                               lk = k;
+                               break;
+                       }
+               }
+               return lk;
+       }
+
+       @Override
+       // find leftmost pre-base matra target, starting from source
+       protected int findPreBaseMatraTarget(GlyphSequence gs, int source) {
+               int ng = gs.getGlyphCount();
+               int lk = -1;
+               for (int i = (source < ng) ? source : ng; i > 0; i--) {
+                       int k = i - 1;
+                       if (containsConsonant(gs, k)) {
+                               if (containsHalfConsonant(gs, k)) {
+                                       lk = k;
+                               } else if (lk == -1) {
+                                       lk = k;
+                               } else {
+                                       break;
+                               }
+                       }
+               }
+               return lk;
+       }
+
+       private static boolean containsPreBaseMatra(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isPreM(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsConsonant(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isC(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsHalfConsonant(GlyphSequence gs, int k) {
+               Boolean half = (Boolean) gs.getAssociation(k).getPredication("half");
+               return (half != null) ? half.booleanValue() : false;
+       }
+
+       @Override
+       protected int findReph(GlyphSequence gs) {
+               int ng = gs.getGlyphCount();
+               int li = -1;
+               for (int i = 0; i < ng; i++) {
+                       if (containsReph(gs, i)) {
+                               li = i;
+                               break;
+                       }
+               }
+               return li;
+       }
+
+       @Override
+       protected int findRephTarget(GlyphSequence gs, int source) {
+               int ng = gs.getGlyphCount();
+               int c1 = -1;
+               int c2 = -1;
+               // first candidate target is after first non-half consonant
+               for (int i = 0; i < ng; i++) {
+                       if ((i != source) && containsConsonant(gs, i)) {
+                               if (!containsHalfConsonant(gs, i)) {
+                                       c1 = i + 1;
+                                       break;
+                               }
+                       }
+               }
+               // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark
+               for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) {
+                       if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) {
+                               c2 = i + 1;
+                       } else if (containsOtherMark(gs, i)) {
+                               c2 = i;
+                               break;
+                       }
+               }
+               if (c2 >= 0) {
+                       return c2;
+               } else if (c1 >= 0) {
+                       return c1;
+               } else {
+                       return source;
+               }
+       }
+
+       private static boolean containsReph(GlyphSequence gs, int k) {
+               Boolean rphf = (Boolean) gs.getAssociation(k).getPredication("rphf");
+               return (rphf != null) ? rphf.booleanValue() : false;
+       }
+
+       private static boolean containsMatra(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isM(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsOtherMark(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       switch (typeOf(ca[i])) {
+                               case C_T:   // tone (e.g., udatta, anudatta)
+                               case C_A:   // accent (e.g., acute, grave)
+                               case C_O:   // other (e.g., candrabindu, anusvara, visarga, etc)
+                                       return true;
+                               default:
+                                       break;
+                       }
+               }
+               return false;
+       }
+
+       private static class GujaratiSyllabizer extends DefaultSyllabizer {
+
+               GujaratiSyllabizer(String script, String language) {
+                       super(script, language);
+               }
+
+               @Override
+               // | C ...
+               protected int findStartOfSyllable(int[] ca, int s, int e) {
+                       if ((s < 0) || (s >= e)) {
+                               return -1;
+                       } else {
+                               while (s < e) {
+                                       int c = ca[s];
+                                       if (isC(c)) {
+                                               break;
+                                       } else {
+                                               s++;
+                                       }
+                               }
+                               return s;
+                       }
+               }
+
+               @Override
+               // D* L? | ...
+               protected int findEndOfSyllable(int[] ca, int s, int e) {
+                       if ((s < 0) || (s >= e)) {
+                               return -1;
+                       } else {
+                               int nd = 0;
+                               int nl = 0;
+                               int i;
+                               // consume dead consonants
+                               while ((i = isDeadConsonant(ca, s, e)) > s) {
+                                       s = i;
+                                       nd++;
+                               }
+                               // consume zero or one live consonant
+                               if ((i = isLiveConsonant(ca, s, e)) > s) {
+                                       s = i;
+                                       nl++;
+                               }
+                               return ((nd > 0) || (nl > 0)) ? s : -1;
+                       }
+               }
+
+               // D := ( C N? H )?
+               private int isDeadConsonant(int[] ca, int s, int e) {
+                       if (s < 0) {
+                               return -1;
+                       } else {
+                               int c;
+                               int i = 0;
+                               int nc = 0;
+                               int nh = 0;
+                               do {
+                                       // C
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isC(c)) {
+                                                       i++;
+                                                       nc++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                                       // N?
+                                       if ((s + i) < e) {
+                                               c = ca[s + 1];
+                                               if (isN(c)) {
+                                                       i++;
+                                               }
+                                       }
+                                       // H
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isH(c)) {
+                                                       i++;
+                                                       nh++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                               } while (false);
+                               return (nc > 0) && (nh > 0) ? s + i : -1;
+                       }
+               }
+
+               // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK )
+               private int isLiveConsonant(int[] ca, int s, int e) {
+                       if (s < 0) {
+                               return -1;
+                       } else {
+                               int c;
+                               int i = 0;
+                               int nc = 0;
+                               int nv = 0;
+                               int nx = 0;
+                               do {
+                                       // C
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isC(c)) {
+                                                       i++;
+                                                       nc++;
+                                               } else if (isV(c)) {
+                                                       i++;
+                                                       nv++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                                       // N?
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isN(c)) {
+                                                       i++;
+                                               }
+                                       }
+                                       // X*
+                                       while ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isX(c)) {
+                                                       i++;
+                                                       nx++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                               } while (false);
+                               // if no X but has H, then ignore C|I
+                               if (nx == 0) {
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isH(c)) {
+                                                       if (nc > 0) {
+                                                               nc--;
+                                                       } else if (nv > 0) {
+                                                               nv--;
+                                                       }
+                                               }
+                                       }
+                               }
+                               return ((nc > 0) || (nv > 0)) ? s + i : -1;
+                       }
+               }
+       }
+
+       // gujarati character types
+       static final short C_U = 0;            // unassigned
+       static final short C_C = 1;            // consonant
+       static final short C_V = 2;            // vowel
+       static final short C_M = 3;            // vowel sign (matra)
+       static final short C_S = 4;            // symbol or sign
+       static final short C_T = 5;            // tone mark
+       static final short C_A = 6;            // accent mark
+       static final short C_P = 7;            // punctuation
+       static final short C_D = 8;            // digit
+       static final short C_H = 9;            // halant (virama)
+       static final short C_O = 10;           // other signs
+       static final short C_N = 0x0100;       // nukta(ized)
+       static final short C_R = 0x0200;       // reph(ized)
+       static final short C_PRE = 0x0400;       // pre-base
+       static final short C_M_TYPE = 0x00FF;       // type mask
+       static final short C_M_FLAGS = 0x7F00;       // flag mask
+       // gujarati block range
+       static final int CCA_START = 0x0A80;      // first code point mapped by cca
+       static final int CCA_END = 0x0B00;      // last code point + 1 mapped by cca
+       // gujarati character type lookups
+       static final short[] CCA = {
+                       C_U,                        // 0x0A80       // UNASSIGNED
+                       C_O,                        // 0x0A81       // CANDRABINDU
+                       C_O,                        // 0x0A82       // ANUSVARA
+                       C_O,                        // 0x0A83       // VISARGA
+                       C_U,                        // 0x0A84       // UNASSIGNED
+                       C_V,                        // 0x0A85       // A
+                       C_V,                        // 0x0A86       // AA
+                       C_V,                        // 0x0A87       // I
+                       C_V,                        // 0x0A88       // II
+                       C_V,                        // 0x0A89       // U
+                       C_V,                        // 0x0A8A       // UU
+                       C_V,                        // 0x0A8B       // VOCALIC R
+                       C_V,                        // 0x0A8C       // VOCALIC L
+                       C_V,                        // 0x0A8D       // CANDRA E
+                       C_U,                        // 0x0A8E       // UNASSIGNED
+                       C_V,                        // 0x0A8F       // E
+                       C_V,                        // 0x0A90       // AI
+                       C_V,                        // 0x0A91       // CANDRA O
+                       C_U,                        // 0x0A92       // UNASSIGNED
+                       C_V,                        // 0x0A93       // O
+                       C_V,                        // 0x0A94       // AU
+                       C_C,                        // 0x0A95       // KA
+                       C_C,                        // 0x0A96       // KHA
+                       C_C,                        // 0x0A97       // GA
+                       C_C,                        // 0x0A98       // GHA
+                       C_C,                        // 0x0A99       // NGA
+                       C_C,                        // 0x0A9A       // CA
+                       C_C,                        // 0x0A9B       // CHA
+                       C_C,                        // 0x0A9C       // JA
+                       C_C,                        // 0x0A9D       // JHA
+                       C_C,                        // 0x0A9E       // NYA
+                       C_C,                        // 0x0A9F       // TTA
+                       C_C,                        // 0x0AA0       // TTHA
+                       C_C,                        // 0x0AA1       // DDA
+                       C_C,                        // 0x0AA2       // DDHA
+                       C_C,                        // 0x0AA3       // NNA
+                       C_C,                        // 0x0AA4       // TA
+                       C_C,                        // 0x0AA5       // THA
+                       C_C,                        // 0x0AA6       // DA
+                       C_C,                        // 0x0AA7       // DHA
+                       C_C,                        // 0x0AA8       // NA
+                       C_U,                        // 0x0AA9       // UNASSIGNED
+                       C_C,                        // 0x0AAA       // PA
+                       C_C,                        // 0x0AAB       // PHA
+                       C_C,                        // 0x0AAC       // BA
+                       C_C,                        // 0x0AAD       // BHA
+                       C_C,                        // 0x0AAE       // MA
+                       C_C,                        // 0x0AAF       // YA
+                       C_C | C_R,                  // 0x0AB0       // RA
+                       C_U,                        // 0x0AB1       // UNASSIGNED
+                       C_C,                        // 0x0AB2       // LA
+                       C_C,                        // 0x0AB3       // LLA
+                       C_U,                        // 0x0AB4       // UNASSIGNED
+                       C_C,                        // 0x0AB5       // VA
+                       C_C,                        // 0x0AB6       // SHA
+                       C_C,                        // 0x0AB7       // SSA
+                       C_C,                        // 0x0AB8       // SA
+                       C_C,                        // 0x0AB9       // HA
+                       C_U,                        // 0x0ABA       // UNASSIGNED
+                       C_U,                        // 0x0ABB       // UNASSIGNED
+                       C_N,                        // 0x0ABC       // NUKTA
+                       C_S,                        // 0x0ABD       // AVAGRAHA
+                       C_M,                        // 0x0ABE       // AA
+                       C_M | C_PRE,                // 0x0ABF       // I
+                       C_M,                        // 0x0AC0       // II
+                       C_M,                        // 0x0AC1       // U
+                       C_M,                        // 0x0AC2       // UU
+                       C_M,                        // 0x0AC3       // VOCALIC R
+                       C_M,                        // 0x0AC4       // VOCALIC RR
+                       C_M,                        // 0x0AC5       // CANDRA E
+                       C_U,                        // 0x0AC6       // UNASSIGNED
+                       C_M,                        // 0x0AC7       // E
+                       C_M,                        // 0x0AC8       // AI
+                       C_M,                        // 0x0AC9       // CANDRA O
+                       C_U,                        // 0x0ACA       // UNASSIGNED
+                       C_M,                        // 0x0ACB       // O
+                       C_M,                        // 0x0ACC       // AU
+                       C_H,                        // 0x0ACD       // VIRAMA (HALANT)
+                       C_U,                        // 0x0ACE       // UNASSIGNED
+                       C_U,                        // 0x0ACF       // UNASSIGNED
+                       C_S,                        // 0x0AD0       // OM
+                       C_U,                        // 0x0AD1       // UNASSIGNED
+                       C_U,                        // 0x0AD2       // UNASSIGNED
+                       C_U,                        // 0x0AD3       // UNASSIGNED
+                       C_U,                        // 0x0AD4       // UNASSIGNED
+                       C_U,                        // 0x0AD5       // UNASSIGNED
+                       C_U,                        // 0x0AD6       // UNASSIGNED
+                       C_U,                        // 0x0AD7       // UNASSIGNED
+                       C_U,                        // 0x0AD8       // UNASSIGNED
+                       C_U,                        // 0x0AD9       // UNASSIGNED
+                       C_U,                        // 0x0ADA       // UNASSIGNED
+                       C_U,                        // 0x0ADB       // UNASSIGNED
+                       C_U,                        // 0x0ADC       // UNASSIGNED
+                       C_U,                        // 0x0ADD       // UNASSIGNED
+                       C_U,                        // 0x0ADE       // UNASSIGNED
+                       C_U,                        // 0x0ADF       // UNASSIGNED
+                       C_V,                        // 0x0AE0       // VOCALIC RR
+                       C_V,                        // 0x0AE1       // VOCALIC LL
+                       C_M,                        // 0x0AE2       // VOCALIC L
+                       C_M,                        // 0x0AE3       // VOCALIC LL
+                       C_U,                        // 0x0AE4       // UNASSIGNED
+                       C_U,                        // 0x0AE5       // UNASSIGNED
+                       C_D,                        // 0x0AE6       // ZERO
+                       C_D,                        // 0x0AE7       // ONE
+                       C_D,                        // 0x0AE8       // TWO
+                       C_D,                        // 0x0AE9       // THREE
+                       C_D,                        // 0x0AEA       // FOUR
+                       C_D,                        // 0x0AEB       // FIVE
+                       C_D,                        // 0x0AEC       // SIX
+                       C_D,                        // 0x0AED       // SEVEN
+                       C_D,                        // 0x0AEE       // EIGHT
+                       C_D,                        // 0x0AEF       // NINE
+                       C_U,                        // 0x0AF0       // UNASSIGNED
+                       C_S,                        // 0x0AF1       // RUPEE SIGN
+                       C_U,                        // 0x0AF2       // UNASSIGNED
+                       C_U,                        // 0x0AF3       // UNASSIGNED
+                       C_U,                        // 0x0AF4       // UNASSIGNED
+                       C_U,                        // 0x0AF5       // UNASSIGNED
+                       C_U,                        // 0x0AF6       // UNASSIGNED
+                       C_U,                        // 0x0AF7       // UNASSIGNED
+                       C_U,                        // 0x0AF8       // UNASSIGNED
+                       C_U,                        // 0x0AF9       // UNASSIGNED
+                       C_U,                        // 0x0AFA       // UNASSIGNED
+                       C_U,                        // 0x0AFB       // UNASSIGNED
+                       C_U,                        // 0x0AFC       // UNASSIGNED
+                       C_U,                        // 0x0AFD       // UNASSIGNED
+                       C_U,                        // 0x0AFE       // UNASSIGNED
+                       C_U                         // 0x0AFF       // UNASSIGNED
+       };
+
+       static int typeOf(int c) {
+               if ((c >= CCA_START) && (c < CCA_END)) {
+                       return CCA[c - CCA_START] & C_M_TYPE;
+               } else {
+                       return C_U;
+               }
+       }
+
+       static boolean isType(int c, int t) {
+               return typeOf(c) == t;
+       }
+
+       static boolean hasFlag(int c, int f) {
+               if ((c >= CCA_START) && (c < CCA_END)) {
+                       return (CCA[c - CCA_START] & f) == f;
+               } else {
+                       return false;
+               }
+       }
+
+       static boolean isC(int c) {
+               return isType(c, C_C);
+       }
+
+       static boolean isR(int c) {
+               return isType(c, C_C) && hasR(c);
+       }
+
+       static boolean isV(int c) {
+               return isType(c, C_V);
+       }
+
+       static boolean isN(int c) {
+               return c == 0x0ABC;
+       }
+
+       static boolean isH(int c) {
+               return c == 0x0ACD;
+       }
+
+       static boolean isM(int c) {
+               return isType(c, C_M);
+       }
+
+       static boolean isPreM(int c) {
+               return isType(c, C_M) && hasFlag(c, C_PRE);
+       }
+
+       static boolean isX(int c) {
+               switch (typeOf(c)) {
+                       case C_M: // matra (combining vowel)
+                       case C_A: // accent mark
+                       case C_T: // tone mark
+                       case C_O: // other (modifying) mark
+                               return true;
+                       default:
+                               return false;
+               }
+       }
+
+       static boolean hasR(int c) {
+               return hasFlag(c, C_R);
+       }
+
+       static boolean hasN(int c) {
+               return hasFlag(c, C_N);
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GurmukhiScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GurmukhiScriptProcessor.java
new file mode 100644 (file)
index 0000000..9531fd9
--- /dev/null
@@ -0,0 +1,548 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.scripts;
+
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+
+/**
+ * <p>The <code>GurmukhiScriptProcessor</code> class implements a script processor for
+ * performing glyph substitution and positioning operations on content associated with the Gurmukhi script.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GurmukhiScriptProcessor extends IndicScriptProcessor {
+
+       GurmukhiScriptProcessor(String script) {
+               super(script);
+       }
+
+       @Override
+       protected Class<? extends GurmukhiSyllabizer> getSyllabizerClass() {
+               return GurmukhiSyllabizer.class;
+       }
+
+       @Override
+       // find rightmost pre-base matra
+       protected int findPreBaseMatra(GlyphSequence gs) {
+               int ng = gs.getGlyphCount();
+               int lk = -1;
+               for (int i = ng; i > 0; i--) {
+                       int k = i - 1;
+                       if (containsPreBaseMatra(gs, k)) {
+                               lk = k;
+                               break;
+                       }
+               }
+               return lk;
+       }
+
+       @Override
+       // find leftmost pre-base matra target, starting from source
+       protected int findPreBaseMatraTarget(GlyphSequence gs, int source) {
+               int ng = gs.getGlyphCount();
+               int lk = -1;
+               for (int i = (source < ng) ? source : ng; i > 0; i--) {
+                       int k = i - 1;
+                       if (containsConsonant(gs, k)) {
+                               if (containsHalfConsonant(gs, k)) {
+                                       lk = k;
+                               } else if (lk == -1) {
+                                       lk = k;
+                               } else {
+                                       break;
+                               }
+                       }
+               }
+               return lk;
+       }
+
+       private static boolean containsPreBaseMatra(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isPreM(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsConsonant(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isC(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsHalfConsonant(GlyphSequence gs, int k) {
+               Boolean half = (Boolean) gs.getAssociation(k).getPredication("half");
+               return (half != null) ? half.booleanValue() : false;
+       }
+
+       @Override
+       protected int findReph(GlyphSequence gs) {
+               int ng = gs.getGlyphCount();
+               int li = -1;
+               for (int i = 0; i < ng; i++) {
+                       if (containsReph(gs, i)) {
+                               li = i;
+                               break;
+                       }
+               }
+               return li;
+       }
+
+       @Override
+       protected int findRephTarget(GlyphSequence gs, int source) {
+               int ng = gs.getGlyphCount();
+               int c1 = -1;
+               int c2 = -1;
+               // first candidate target is after first non-half consonant
+               for (int i = 0; i < ng; i++) {
+                       if ((i != source) && containsConsonant(gs, i)) {
+                               if (!containsHalfConsonant(gs, i)) {
+                                       c1 = i + 1;
+                                       break;
+                               }
+                       }
+               }
+               // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark
+               for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) {
+                       if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) {
+                               c2 = i + 1;
+                       } else if (containsOtherMark(gs, i)) {
+                               c2 = i;
+                               break;
+                       }
+               }
+               if (c2 >= 0) {
+                       return c2;
+               } else if (c1 >= 0) {
+                       return c1;
+               } else {
+                       return source;
+               }
+       }
+
+       private static boolean containsReph(GlyphSequence gs, int k) {
+               Boolean rphf = (Boolean) gs.getAssociation(k).getPredication("rphf");
+               return (rphf != null) ? rphf.booleanValue() : false;
+       }
+
+       private static boolean containsMatra(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isM(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsOtherMark(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       switch (typeOf(ca[i])) {
+                               case C_T:   // tone (e.g., udatta, anudatta)
+                               case C_A:   // accent (e.g., acute, grave)
+                               case C_O:   // other (e.g., candrabindu, anusvara, visarga, etc)
+                                       return true;
+                               default:
+                                       break;
+                       }
+               }
+               return false;
+       }
+
+       private static class GurmukhiSyllabizer extends DefaultSyllabizer {
+
+               GurmukhiSyllabizer(String script, String language) {
+                       super(script, language);
+               }
+
+               @Override
+               // | C ...
+               protected int findStartOfSyllable(int[] ca, int s, int e) {
+                       if ((s < 0) || (s >= e)) {
+                               return -1;
+                       } else {
+                               while (s < e) {
+                                       int c = ca[s];
+                                       if (isC(c)) {
+                                               break;
+                                       } else {
+                                               s++;
+                                       }
+                               }
+                               return s;
+                       }
+               }
+
+               @Override
+               // D* L? | ...
+               protected int findEndOfSyllable(int[] ca, int s, int e) {
+                       if ((s < 0) || (s >= e)) {
+                               return -1;
+                       } else {
+                               int nd = 0;
+                               int nl = 0;
+                               int i;
+                               // consume dead consonants
+                               while ((i = isDeadConsonant(ca, s, e)) > s) {
+                                       s = i;
+                                       nd++;
+                               }
+                               // consume zero or one live consonant
+                               if ((i = isLiveConsonant(ca, s, e)) > s) {
+                                       s = i;
+                                       nl++;
+                               }
+                               return ((nd > 0) || (nl > 0)) ? s : -1;
+                       }
+               }
+
+               // D := ( C N? H )?
+               private int isDeadConsonant(int[] ca, int s, int e) {
+                       if (s < 0) {
+                               return -1;
+                       } else {
+                               int c;
+                               int i = 0;
+                               int nc = 0;
+                               int nh = 0;
+                               do {
+                                       // C
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isC(c)) {
+                                                       i++;
+                                                       nc++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                                       // N?
+                                       if ((s + i) < e) {
+                                               c = ca[s + 1];
+                                               if (isN(c)) {
+                                                       i++;
+                                               }
+                                       }
+                                       // H
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isH(c)) {
+                                                       i++;
+                                                       nh++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                               } while (false);
+                               return (nc > 0) && (nh > 0) ? s + i : -1;
+                       }
+               }
+
+               // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK )
+               private int isLiveConsonant(int[] ca, int s, int e) {
+                       if (s < 0) {
+                               return -1;
+                       } else {
+                               int c;
+                               int i = 0;
+                               int nc = 0;
+                               int nv = 0;
+                               int nx = 0;
+                               do {
+                                       // C
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isC(c)) {
+                                                       i++;
+                                                       nc++;
+                                               } else if (isV(c)) {
+                                                       i++;
+                                                       nv++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                                       // N?
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isN(c)) {
+                                                       i++;
+                                               }
+                                       }
+                                       // X*
+                                       while ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isX(c)) {
+                                                       i++;
+                                                       nx++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                               } while (false);
+                               // if no X but has H, then ignore C|I
+                               if (nx == 0) {
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isH(c)) {
+                                                       if (nc > 0) {
+                                                               nc--;
+                                                       } else if (nv > 0) {
+                                                               nv--;
+                                                       }
+                                               }
+                                       }
+                               }
+                               return ((nc > 0) || (nv > 0)) ? s + i : -1;
+                       }
+               }
+       }
+
+       // gurmukhi character types
+       static final short C_U = 0;            // unassigned
+       static final short C_C = 1;            // consonant
+       static final short C_V = 2;            // vowel
+       static final short C_M = 3;            // vowel sign (matra)
+       static final short C_S = 4;            // symbol or sign
+       static final short C_T = 5;            // tone mark
+       static final short C_A = 6;            // accent mark
+       static final short C_P = 7;            // punctuation
+       static final short C_D = 8;            // digit
+       static final short C_H = 9;            // halant (virama)
+       static final short C_O = 10;           // other signs
+       static final short C_N = 0x0100;       // nukta(ized)
+       static final short C_R = 0x0200;       // reph(ized)
+       static final short C_PRE = 0x0400;       // pre-base
+       static final short C_M_TYPE = 0x00FF;       // type mask
+       static final short C_M_FLAGS = 0x7F00;       // flag mask
+       // gurmukhi block range
+       static final int CCA_START = 0x0A00;      // first code point mapped by cca
+       static final int CCA_END = 0x0A80;      // last code point + 1 mapped by cca
+       // gurmukhi character type lookups
+       static final short[] CCA = {
+                       C_U,                        // 0x0A00       // UNASSIGNED
+                       C_O,                        // 0x0A01       // ADAK BINDI
+                       C_O,                        // 0x0A02       // BINDI
+                       C_O,                        // 0x0A03       // VISARGA
+                       C_U,                        // 0x0A04       // UNASSIGNED
+                       C_V,                        // 0x0A05       // A
+                       C_V,                        // 0x0A06       // AA
+                       C_V,                        // 0x0A07       // I
+                       C_V,                        // 0x0A08       // II
+                       C_V,                        // 0x0A09       // U
+                       C_V,                        // 0x0A0A       // UU
+                       C_U,                        // 0x0A0B       // UNASSIGNED
+                       C_U,                        // 0x0A0C       // UNASSIGNED
+                       C_U,                        // 0x0A0D       // UNASSIGNED
+                       C_U,                        // 0x0A0E       // UNASSIGNED
+                       C_V,                        // 0x0A0F       // E
+                       C_V,                        // 0x0A10       // AI
+                       C_U,                        // 0x0A11       // UNASSIGNED
+                       C_U,                        // 0x0A12       // UNASSIGNED
+                       C_V,                        // 0x0A13       // O
+                       C_V,                        // 0x0A14       // AU
+                       C_C,                        // 0x0A15       // KA
+                       C_C,                        // 0x0A16       // KHA
+                       C_C,                        // 0x0A17       // GA
+                       C_C,                        // 0x0A18       // GHA
+                       C_C,                        // 0x0A19       // NGA
+                       C_C,                        // 0x0A1A       // CA
+                       C_C,                        // 0x0A1B       // CHA
+                       C_C,                        // 0x0A1C       // JA
+                       C_C,                        // 0x0A1D       // JHA
+                       C_C,                        // 0x0A1E       // NYA
+                       C_C,                        // 0x0A1F       // TTA
+                       C_C,                        // 0x0A20       // TTHA
+                       C_C,                        // 0x0A21       // DDA
+                       C_C,                        // 0x0A22       // DDHA
+                       C_C,                        // 0x0A23       // NNA
+                       C_C,                        // 0x0A24       // TA
+                       C_C,                        // 0x0A25       // THA
+                       C_C,                        // 0x0A26       // DA
+                       C_C,                        // 0x0A27       // DHA
+                       C_C,                        // 0x0A28       // NA
+                       C_U,                        // 0x0A29       // UNASSIGNED
+                       C_C,                        // 0x0A2A       // PA
+                       C_C,                        // 0x0A2B       // PHA
+                       C_C,                        // 0x0A2C       // BA
+                       C_C,                        // 0x0A2D       // BHA
+                       C_C,                        // 0x0A2E       // MA
+                       C_C,                        // 0x0A2F       // YA
+                       C_C | C_R,                  // 0x0A30       // RA
+                       C_U,                        // 0x0A31       // UNASSIGNED
+                       C_C,                        // 0x0A32       // LA
+                       C_C,                        // 0x0A33       // LLA
+                       C_U,                        // 0x0A34       // UNASSIGNED
+                       C_C,                        // 0x0A35       // VA
+                       C_C,                        // 0x0A36       // SHA
+                       C_U,                        // 0x0A37       // UNASSIGNED
+                       C_C,                        // 0x0A38       // SA
+                       C_C,                        // 0x0A39       // HA
+                       C_U,                        // 0x0A3A       // UNASSIGNED
+                       C_U,                        // 0x0A3B       // UNASSIGNED
+                       C_N,                        // 0x0A3C       // NUKTA
+                       C_U,                        // 0x0A3D       // UNASSIGNED
+                       C_M,                        // 0x0A3E       // AA
+                       C_M | C_PRE,                // 0x0A3F       // I
+                       C_M,                        // 0x0A40       // II
+                       C_M,                        // 0x0A41       // U
+                       C_M,                        // 0x0A42       // UU
+                       C_U,                        // 0x0A43       // UNASSIGNED
+                       C_U,                        // 0x0A44       // UNASSIGNED
+                       C_U,                        // 0x0A45       // UNASSIGNED
+                       C_U,                        // 0x0A46       // UNASSIGNED
+                       C_M,                        // 0x0A47       // EE
+                       C_M,                        // 0x0A48       // AI
+                       C_U,                        // 0x0A49       // UNASSIGNED
+                       C_U,                        // 0x0A4A       // UNASSIGNED
+                       C_M,                        // 0x0A4B       // OO
+                       C_M,                        // 0x0A4C       // AU
+                       C_H,                        // 0x0A4D       // VIRAMA (HALANT)
+                       C_U,                        // 0x0A4E       // UNASSIGNED
+                       C_U,                        // 0x0A4F       // UNASSIGNED
+                       C_U,                        // 0x0A50       // UNASSIGNED
+                       C_T,                        // 0x0A51       // UDATTA
+                       C_U,                        // 0x0A52       // UNASSIGNED
+                       C_U,                        // 0x0A53       // UNASSIGNED
+                       C_U,                        // 0x0A54       // UNASSIGNED
+                       C_U,                        // 0x0A55       // UNASSIGNED
+                       C_U,                        // 0x0A56       // UNASSIGNED
+                       C_U,                        // 0x0A57       // UNASSIGNED
+                       C_U,                        // 0x0A58       // UNASSIGNED
+                       C_C | C_N,                  // 0x0A59       // KHHA
+                       C_C | C_N,                  // 0x0A5A       // GHHA
+                       C_C | C_N,                  // 0x0A5B       // ZA
+                       C_C | C_N,                  // 0x0A5C       // RRA
+                       C_U,                        // 0x0A5D       // UNASSIGNED
+                       C_C | C_N,                  // 0x0A5E       // FA
+                       C_U,                        // 0x0A5F       // UNASSIGNED
+                       C_U,                        // 0x0A60       // UNASSIGNED
+                       C_U,                        // 0x0A61       // UNASSIGNED
+                       C_U,                        // 0x0A62       // UNASSIGNED
+                       C_U,                        // 0x0A63       // UNASSIGNED
+                       C_U,                        // 0x0A64       // UNASSIGNED
+                       C_U,                        // 0x0A65       // UNASSIGNED
+                       C_D,                        // 0x0A66       // ZERO
+                       C_D,                        // 0x0A67       // ONE
+                       C_D,                        // 0x0A68       // TWO
+                       C_D,                        // 0x0A69       // THREE
+                       C_D,                        // 0x0A6A       // FOUR
+                       C_D,                        // 0x0A6B       // FIVE
+                       C_D,                        // 0x0A6C       // SIX
+                       C_D,                        // 0x0A6D       // SEVEN
+                       C_D,                        // 0x0A6E       // EIGHT
+                       C_D,                        // 0x0A6F       // NINE
+                       C_O,                        // 0x0A70       // TIPPI
+                       C_O,                        // 0x0A71       // ADDAK
+                       C_V,                        // 0x0A72       // IRI
+                       C_V,                        // 0x0A73       // URA
+                       C_S,                        // 0x0A74       // EK ONKAR
+                       C_O,                        // 0x0A75       // YAKASH
+                       C_U,                        // 0x0A76       // UNASSIGNED
+                       C_U,                        // 0x0A77       // UNASSIGNED
+                       C_U,                        // 0x0A78       // UNASSIGNED
+                       C_U,                        // 0x0A79       // UNASSIGNED
+                       C_U,                        // 0x0A7A       // UNASSIGNED
+                       C_U,                        // 0x0A7B       // UNASSIGNED
+                       C_U,                        // 0x0A7C       // UNASSIGNED
+                       C_U,                        // 0x0A7D       // UNASSIGNED
+                       C_U,                        // 0x0A7E       // UNASSIGNED
+                       C_U                         // 0x0A7F       // UNASSIGNED
+       };
+
+       static int typeOf(int c) {
+               if ((c >= CCA_START) && (c < CCA_END)) {
+                       return CCA[c - CCA_START] & C_M_TYPE;
+               } else {
+                       return C_U;
+               }
+       }
+
+       static boolean isType(int c, int t) {
+               return typeOf(c) == t;
+       }
+
+       static boolean hasFlag(int c, int f) {
+               if ((c >= CCA_START) && (c < CCA_END)) {
+                       return (CCA[c - CCA_START] & f) == f;
+               } else {
+                       return false;
+               }
+       }
+
+       static boolean isC(int c) {
+               return isType(c, C_C);
+       }
+
+       static boolean isR(int c) {
+               return isType(c, C_C) && hasR(c);
+       }
+
+       static boolean isV(int c) {
+               return isType(c, C_V);
+       }
+
+       static boolean isN(int c) {
+               return c == 0x0A3C;
+       }
+
+       static boolean isH(int c) {
+               return c == 0x0A4D;
+       }
+
+       static boolean isM(int c) {
+               return isType(c, C_M);
+       }
+
+       static boolean isPreM(int c) {
+               return isType(c, C_M) && hasFlag(c, C_PRE);
+       }
+
+       static boolean isX(int c) {
+               switch (typeOf(c)) {
+                       case C_M: // matra (combining vowel)
+                       case C_A: // accent mark
+                       case C_T: // tone mark
+                       case C_O: // other (modifying) mark
+                               return true;
+                       default:
+                               return false;
+               }
+       }
+
+       static boolean hasR(int c) {
+               return hasFlag(c, C_R);
+       }
+
+       static boolean hasN(int c) {
+               return hasFlag(c, C_N);
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/IndicScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/IndicScriptProcessor.java
new file mode 100644 (file)
index 0000000..950e589
--- /dev/null
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.scripts;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphContextTester;
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+import com.jaredrummler.fontreader.complexscripts.util.CharScript;
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.ScriptContextTester;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.*;
+
+// CSOFF: LineLengthCheck
+
+/**
+ * <p>The <code>IndicScriptProcessor</code> class implements a script processor for
+ * performing glyph substitution and positioning operations on content associated with the Indic script.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class IndicScriptProcessor extends DefaultScriptProcessor {
+
+       /**
+        * required features to use for substitutions
+        */
+       private static final String[] GSUB_REQ_FEATURES =
+                       {
+                                       "abvf",                                                 // above base forms
+                                       "abvs",                                                 // above base substitutions
+                                       "akhn",                                                 // akhand
+                                       "blwf",                                                 // below base forms
+                                       "blws",                                                 // below base substitutions
+                                       "ccmp",                                                 // glyph composition/decomposition
+                                       "cjct",                                                 // conjunct forms
+                                       "clig",                                                 // contextual ligatures
+                                       "half",                                                 // half forms
+                                       "haln",                                                 // halant forms
+                                       "locl",                                                 // localized forms
+                                       "nukt",                                                 // nukta forms
+                                       "pref",                                                 // pre-base forms
+                                       "pres",                                                 // pre-base substitutions
+                                       "pstf",                                                 // post-base forms
+                                       "psts",                                                 // post-base substitutions
+                                       "rkrf",                                                 // rakar forms
+                                       "rphf",                                                 // reph form
+                                       "vatu"                                                  // vattu variants
+                       };
+
+       /**
+        * optional features to use for substitutions
+        */
+       private static final String[] GSUB_OPT_FEATURES =
+                       {
+                                       "afrc",                                                 // alternative fractions
+                                       "calt",                                                 // contextual alternatives
+                                       "dlig"                                                  // discretionary ligatures
+                       };
+
+       /**
+        * required features to use for positioning
+        */
+       private static final String[] GPOS_REQ_FEATURES =
+                       {
+                                       "abvm",                                                 // above base marks
+                                       "blwm",                                                 // below base marks
+                                       "dist",                                                 // distance (adjustment)
+                                       "kern"                                                  // kerning
+                       };
+
+       /**
+        * required features to use for positioning
+        */
+       private static final String[] GPOS_OPT_FEATURES =
+                       {
+                       };
+
+       private static class SubstitutionScriptContextTester implements ScriptContextTester {
+
+               private static Map/*<String,GlyphContextTester>*/ testerMap = new HashMap/*<String,GlyphContextTester>*/();
+
+               public GlyphContextTester getTester(String feature) {
+                       return (GlyphContextTester) testerMap.get(feature);
+               }
+       }
+
+       private static class PositioningScriptContextTester implements ScriptContextTester {
+
+               private static Map/*<String,GlyphContextTester>*/ testerMap = new HashMap/*<String,GlyphContextTester>*/();
+
+               public GlyphContextTester getTester(String feature) {
+                       return (GlyphContextTester) testerMap.get(feature);
+               }
+       }
+
+       /**
+        * Make script specific flavor of Indic script processor.
+        *
+        * @param script tag
+        * @return script processor instance
+        */
+       public static ScriptProcessor makeProcessor(String script) {
+               switch (CharScript.scriptCodeFromTag(script)) {
+                       case CharScript.SCRIPT_DEVANAGARI:
+                       case CharScript.SCRIPT_DEVANAGARI_2:
+                               return new DevanagariScriptProcessor(script);
+                       case CharScript.SCRIPT_GUJARATI:
+                       case CharScript.SCRIPT_GUJARATI_2:
+                               return new GujaratiScriptProcessor(script);
+                       case CharScript.SCRIPT_GURMUKHI:
+                       case CharScript.SCRIPT_GURMUKHI_2:
+                               return new GurmukhiScriptProcessor(script);
+                       case CharScript.SCRIPT_TAMIL:
+                       case CharScript.SCRIPT_TAMIL_2:
+                               return new TamilScriptProcessor(script);
+                       // [TBD] implement other script processors
+                       default:
+                               return new IndicScriptProcessor(script);
+               }
+       }
+
+       private final ScriptContextTester subContextTester;
+       private final ScriptContextTester posContextTester;
+
+       IndicScriptProcessor(String script) {
+               super(script);
+               this.subContextTester = new SubstitutionScriptContextTester();
+               this.posContextTester = new PositioningScriptContextTester();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String[] getSubstitutionFeatures() {
+               return GSUB_REQ_FEATURES;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String[] getOptionalSubstitutionFeatures() {
+               return GSUB_OPT_FEATURES;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public ScriptContextTester getSubstitutionContextTester() {
+               return subContextTester;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String[] getPositioningFeatures() {
+               return GPOS_REQ_FEATURES;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String[] getOptionalPositioningFeatures() {
+               return GPOS_OPT_FEATURES;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public ScriptContextTester getPositioningContextTester() {
+               return posContextTester;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public GlyphSequence substitute(GlyphSequence gs, String script, String language, GlyphTable.UseSpec[] usa,
+                                                                       ScriptContextTester sct) {
+               assert usa != null;
+               // 1. syllabize
+               GlyphSequence[] sa = syllabize(gs, script, language);
+               // 2. process each syllable
+               for (int i = 0, n = sa.length; i < n; i++) {
+                       GlyphSequence s = sa[i];
+                       // apply basic shaping subs
+                       for (int j = 0, m = usa.length; j < m; j++) {
+                               GlyphTable.UseSpec us = usa[j];
+                               if (isBasicShapingUse(us)) {
+                                       s.setPredications(true);
+                                       s = us.substitute(s, script, language, sct);
+                               }
+                       }
+                       // reorder pre-base matra
+                       s = reorderPreBaseMatra(s);
+                       // reorder reph
+                       s = reorderReph(s);
+                       // apply presentation subs
+                       for (int j = 0, m = usa.length; j < m; j++) {
+                               GlyphTable.UseSpec us = usa[j];
+                               if (isPresentationUse(us)) {
+                                       s.setPredications(true);
+                                       s = us.substitute(s, script, language, sct);
+                               }
+                       }
+                       // record result
+                       sa[i] = s;
+               }
+               // 3. return reassembled substituted syllables
+               return unsyllabize(gs, sa);
+       }
+
+       /**
+        * Get script specific syllabizer class.
+        *
+        * @return a syllabizer class object or null
+        */
+       protected Class<? extends Syllabizer> getSyllabizerClass() {
+               return null;
+       }
+
+       private GlyphSequence[] syllabize(GlyphSequence gs, String script, String language) {
+               return Syllabizer.getSyllabizer(script, language, getSyllabizerClass()).syllabize(gs);
+       }
+
+       private GlyphSequence unsyllabize(GlyphSequence gs, GlyphSequence[] sa) {
+               return GlyphSequence.join(gs, sa);
+       }
+
+       private static Set<String> basicShapingFeatures;
+       private static final String[] BASIC_SHAPING_FEATURE_STRINGS = {
+                       "abvf",
+                       "akhn",
+                       "blwf",
+                       "cjct",
+                       "half",
+                       "locl",
+                       "nukt",
+                       "pref",
+                       "pstf",
+                       "rkrf",
+                       "rphf",
+                       "vatu",
+       };
+
+       static {
+               basicShapingFeatures = new HashSet<String>();
+               for (String s : BASIC_SHAPING_FEATURE_STRINGS) {
+                       basicShapingFeatures.add(s);
+               }
+       }
+
+       private boolean isBasicShapingUse(GlyphTable.UseSpec us) {
+               assert us != null;
+               if (basicShapingFeatures != null) {
+                       return basicShapingFeatures.contains(us.getFeature());
+               } else {
+                       return false;
+               }
+       }
+
+       private static Set<String> presentationFeatures;
+       private static final String[] PRESENTATION_FEATURE_STRINGS = {
+                       "abvs",
+                       "blws",
+                       "calt",
+                       "haln",
+                       "pres",
+                       "psts",
+       };
+
+       static {
+               presentationFeatures = new HashSet<String>();
+               for (String s : PRESENTATION_FEATURE_STRINGS) {
+                       presentationFeatures.add(s);
+               }
+       }
+
+       private boolean isPresentationUse(GlyphTable.UseSpec us) {
+               assert us != null;
+               if (presentationFeatures != null) {
+                       return presentationFeatures.contains(us.getFeature());
+               } else {
+                       return false;
+               }
+       }
+
+       private GlyphSequence reorderPreBaseMatra(GlyphSequence gs) {
+               int source;
+               if ((source = findPreBaseMatra(gs)) >= 0) {
+                       int target;
+                       if ((target = findPreBaseMatraTarget(gs, source)) >= 0) {
+                               if (target != source) {
+                                       gs = reorder(gs, source, target);
+                               }
+                       }
+               }
+               return gs;
+       }
+
+       /**
+        * Find pre-base matra in sequence.
+        *
+        * @param gs input sequence
+        * @return index of pre-base matra or -1 if not found
+        */
+       protected int findPreBaseMatra(GlyphSequence gs) {
+               return -1;
+       }
+
+       /**
+        * Find pre-base matra target in sequence.
+        *
+        * @param gs     input sequence
+        * @param source index of pre-base matra
+        * @return index of pre-base matra target or -1
+        */
+       protected int findPreBaseMatraTarget(GlyphSequence gs, int source) {
+               return -1;
+       }
+
+       private GlyphSequence reorderReph(GlyphSequence gs) {
+               int source;
+               if ((source = findReph(gs)) >= 0) {
+                       int target;
+                       if ((target = findRephTarget(gs, source)) >= 0) {
+                               if (target != source) {
+                                       gs = reorder(gs, source, target);
+                               }
+                       }
+               }
+               return gs;
+       }
+
+       /**
+        * Find reph in sequence.
+        *
+        * @param gs input sequence
+        * @return index of reph or -1 if not found
+        */
+       protected int findReph(GlyphSequence gs) {
+               return -1;
+       }
+
+       /**
+        * Find reph target in sequence.
+        *
+        * @param gs     input sequence
+        * @param source index of reph
+        * @return index of reph target or -1
+        */
+       protected int findRephTarget(GlyphSequence gs, int source) {
+               return -1;
+       }
+
+       private GlyphSequence reorder(GlyphSequence gs, int source, int target) {
+               return GlyphSequence.reorder(gs, source, 1, target);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean position(GlyphSequence gs, String script, String language, int fontSize, GlyphTable.UseSpec[] usa,
+                                                       int[] widths, int[][] adjustments, ScriptContextTester sct) {
+               boolean adjusted = super.position(gs, script, language, fontSize, usa, widths, adjustments, sct);
+               return adjusted;
+       }
+
+       /**
+        * Abstract syllabizer.
+        */
+       protected abstract static class Syllabizer implements Comparable {
+
+               private String script;
+               private String language;
+
+               Syllabizer(String script, String language) {
+                       this.script = script;
+                       this.language = language;
+               }
+
+               /**
+                * Subdivide glyph sequence GS into syllabic segments each represented by a distinct
+                * output glyph sequence.
+                *
+                * @param gs input glyph sequence
+                * @return segmented syllabic glyph sequences
+                */
+               abstract GlyphSequence[] syllabize(GlyphSequence gs);
+
+               /**
+                * {@inheritDoc}
+                */
+               public int hashCode() {
+                       int hc = 0;
+                       hc = 7 * hc + (hc ^ script.hashCode());
+                       hc = 11 * hc + (hc ^ language.hashCode());
+                       return hc;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean equals(Object o) {
+                       if (o instanceof Syllabizer) {
+                               Syllabizer s = (Syllabizer) o;
+                               if (!s.script.equals(script)) {
+                                       return false;
+                               } else {
+                                       return s.language.equals(language);
+                               }
+                       } else {
+                               return false;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int compareTo(Object o) {
+                       int d;
+                       if (o instanceof Syllabizer) {
+                               Syllabizer s = (Syllabizer) o;
+                               if ((d = script.compareTo(s.script)) == 0) {
+                                       d = language.compareTo(s.language);
+                               }
+                       } else {
+                               d = -1;
+                       }
+                       return d;
+               }
+
+               private static Map<String, Syllabizer> syllabizers = new HashMap<String, Syllabizer>();
+
+               static Syllabizer getSyllabizer(String script, String language, Class<? extends Syllabizer> syllabizerClass) {
+                       String sid = makeSyllabizerId(script, language);
+                       Syllabizer s = syllabizers.get(sid);
+                       if (s == null) {
+                               if ((syllabizerClass == null) || ((s = makeSyllabizer(script, language, syllabizerClass)) == null)) {
+                                       s = new DefaultSyllabizer(script, language);
+                               }
+                               syllabizers.put(sid, s);
+                       }
+                       return s;
+               }
+
+               static String makeSyllabizerId(String script, String language) {
+                       return script + ":" + language;
+               }
+
+               static Syllabizer makeSyllabizer(String script, String language, Class<? extends Syllabizer> syllabizerClass) {
+                       Syllabizer s;
+                       try {
+                               Constructor<? extends Syllabizer> cf =
+                                               syllabizerClass.getDeclaredConstructor(String.class, String.class);
+                               s = cf.newInstance(script, language);
+                       } catch (NoSuchMethodException e) {
+                               s = null;
+                       } catch (InstantiationException e) {
+                               s = null;
+                       } catch (IllegalAccessException e) {
+                               s = null;
+                       } catch (InvocationTargetException e) {
+                               s = null;
+                       }
+                       return s;
+               }
+       }
+
+       /**
+        * Default syllabizer.
+        */
+       protected static class DefaultSyllabizer extends Syllabizer {
+
+               DefaultSyllabizer(String script, String language) {
+                       super(script, language);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               GlyphSequence[] syllabize(GlyphSequence gs) {
+                       int[] ca = gs.getCharacterArray(false);
+                       int nc = gs.getCharacterCount();
+                       if (nc == 0) {
+                               return new GlyphSequence[]{gs};
+                       } else {
+                               return segmentize(gs, segmentize(ca, nc));
+                       }
+               }
+
+               /**
+                * Construct array of segements from original character array (associated with original glyph sequence)
+                *
+                * @param ca input character sequence
+                * @param nc number of characters in sequence
+                * @return array of syllable segments
+                */
+               protected Segment[] segmentize(int[] ca, int nc) {
+                       Vector<Segment> sv = new Vector<Segment>(nc);
+                       for (int s = 0, e = nc; s < e; ) {
+                               int i;
+                               if ((i = findStartOfSyllable(ca, s, e)) < e) {
+                                       if (s < i) {
+                                               // from s to i is non-syllable segment
+                                               sv.add(new Segment(s, i, Segment.OTHER));
+                                       }
+                                       s = i; // move s to start of syllable
+                               } else {
+                                       if (s < e) {
+                                               // from s to e is non-syllable segment
+                                               sv.add(new Segment(s, e, Segment.OTHER));
+                                       }
+                                       s = e; // move s to end of input sequence
+                               }
+                               if ((i = findEndOfSyllable(ca, s, e)) > s) {
+                                       if (s < i) {
+                                               // from s to i is syllable segment
+                                               sv.add(new Segment(s, i, Segment.SYLLABLE));
+                                       }
+                                       s = i; // move s to end of syllable
+                               } else {
+                                       if (s < e) {
+                                               // from s to e is non-syllable segment
+                                               sv.add(new Segment(s, e, Segment.OTHER));
+                                       }
+                                       s = e; // move s to end of input sequence
+                               }
+                       }
+                       return sv.toArray(new Segment[sv.size()]);
+               }
+
+               /**
+                * Construct array of glyph sequences from original glyph sequence and segment array.
+                *
+                * @param gs original input glyph sequence
+                * @param sa segment array
+                * @return array of glyph sequences each belonging to an (ordered) segment in SA
+                */
+               protected GlyphSequence[] segmentize(GlyphSequence gs, Segment[] sa) {
+                       int ng = gs.getGlyphCount();
+                       int[] ga = gs.getGlyphArray(false);
+                       CharAssociation[] aa = gs.getAssociations(0, -1);
+                       Vector<GlyphSequence> nsv = new Vector<GlyphSequence>();
+                       for (int i = 0, ns = sa.length; i < ns; i++) {
+                               Segment s = sa[i];
+                               Vector<Integer> ngv = new Vector<Integer>(ng);
+                               Vector<CharAssociation> nav = new Vector<CharAssociation>(ng);
+                               for (int j = 0; j < ng; j++) {
+                                       CharAssociation ca = aa[j];
+                                       if (ca.contained(s.getOffset(), s.getCount())) {
+                                               ngv.add(ga[j]);
+                                               nav.add(ca);
+                                       }
+                               }
+                               if (ngv.size() > 0) {
+                                       nsv.add(new GlyphSequence(gs, null, toIntArray(ngv), null, null, nav.toArray(new CharAssociation[nav.size()]),
+                                                       null));
+                               }
+                       }
+                       if (nsv.size() > 0) {
+                               return nsv.toArray(new GlyphSequence[nsv.size()]);
+                       } else {
+                               return new GlyphSequence[]{gs};
+                       }
+               }
+
+               /**
+                * Find start of syllable in character array, starting at S, ending at E.
+                *
+                * @param ca character array
+                * @param s  start index
+                * @param e  end index
+                * @return index of start or E if no start found
+                */
+               protected int findStartOfSyllable(int[] ca, int s, int e) {
+                       return e;
+               }
+
+               /**
+                * Find end of syllable in character array, starting at S, ending at E.
+                *
+                * @param ca character array
+                * @param s  start index
+                * @param e  end index
+                * @return index of start or S if no end found
+                */
+               protected int findEndOfSyllable(int[] ca, int s, int e) {
+                       return s;
+               }
+
+               private static int[] toIntArray(Vector<Integer> iv) {
+                       int ni = iv.size();
+                       int[] ia = new int[iv.size()];
+                       for (int i = 0, n = ni; i < n; i++) {
+                               ia[i] = iv.get(i);
+                       }
+                       return ia;
+               }
+       }
+
+       /**
+        * Syllabic segment.
+        */
+       protected static class Segment {
+
+               static final int OTHER = 0;            // other (non-syllable) characters
+               static final int SYLLABLE = 1;         // (orthographic) syllable
+
+               private int start;
+               private int end;
+               private int type;
+
+               Segment(int start, int end, int type) {
+                       this.start = start;
+                       this.end = end;
+                       this.type = type;
+               }
+
+               int getStart() {
+                       return start;
+               }
+
+               int getEnd() {
+                       return end;
+               }
+
+               int getOffset() {
+                       return start;
+               }
+
+               int getCount() {
+                       return end - start;
+               }
+
+               int getType() {
+                       return type;
+               }
+       }
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ScriptProcessor.java
new file mode 100644 (file)
index 0000000..9232f21
--- /dev/null
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.scripts;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphDefinitionTable;
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphPositioningTable;
+import com.jaredrummler.fontreader.complexscripts.util.CharScript;
+import com.jaredrummler.fontreader.fonts.GlyphSubstitutionTable;
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.ScriptContextTester;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <p>Abstract script processor base class for which an implementation of the substitution and positioning methods
+ * must be supplied.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public abstract class ScriptProcessor {
+
+       private final String script;
+
+       private final Map<AssembledLookupsKey, GlyphTable.UseSpec[]> assembledLookups;
+
+       private static Map<String, ScriptProcessor> processors = new HashMap<String, ScriptProcessor>();
+
+       /**
+        * Instantiate a script processor.
+        *
+        * @param script a script identifier
+        */
+       protected ScriptProcessor(String script) {
+               if ((script == null) || (script.length() == 0)) {
+                       throw new IllegalArgumentException("script must be non-empty string");
+               } else {
+                       this.script = script;
+                       this.assembledLookups = new HashMap<>();
+               }
+       }
+
+       /**
+        * @return script identifier
+        */
+       public final String getScript() {
+               return script;
+       }
+
+       /**
+        * Obtain script specific required substitution features.
+        *
+        * @return array of suppported substitution features or null
+        */
+       public abstract String[] getSubstitutionFeatures();
+
+       /**
+        * Obtain script specific optional substitution features.
+        *
+        * @return array of suppported substitution features or null
+        */
+       public String[] getOptionalSubstitutionFeatures() {
+               return new String[0];
+       }
+
+       /**
+        * Obtain script specific substitution context tester.
+        *
+        * @return substitution context tester or null
+        */
+       public abstract ScriptContextTester getSubstitutionContextTester();
+
+       /**
+        * Perform substitution processing using a specific set of lookup tables.
+        *
+        * @param gsub     the glyph substitution table that applies
+        * @param gs       an input glyph sequence
+        * @param script   a script identifier
+        * @param language a language identifier
+        * @param lookups  a mapping from lookup specifications to glyph subtables to use for substitution processing
+        * @return the substituted (output) glyph sequence
+        */
+       public final GlyphSequence substitute(GlyphSubstitutionTable gsub, GlyphSequence gs, String script, String language,
+                                                                                 Map/*<LookupSpec,List<LookupTable>>>*/ lookups) {
+               return substitute(gs, script, language, assembleLookups(gsub, getSubstitutionFeatures(), lookups),
+                               getSubstitutionContextTester());
+       }
+
+       /**
+        * Perform substitution processing using a specific set of ordered glyph table use specifications.
+        *
+        * @param gs       an input glyph sequence
+        * @param script   a script identifier
+        * @param language a language identifier
+        * @param usa      an ordered array of glyph table use specs
+        * @param sct      a script specific context tester (or null)
+        * @return the substituted (output) glyph sequence
+        */
+       public GlyphSequence substitute(GlyphSequence gs, String script, String language, GlyphTable.UseSpec[] usa,
+                                                                       ScriptContextTester sct) {
+               assert usa != null;
+               for (int i = 0, n = usa.length; i < n; i++) {
+                       GlyphTable.UseSpec us = usa[i];
+                       gs = us.substitute(gs, script, language, sct);
+               }
+               return gs;
+       }
+
+       /**
+        * Reorder combining marks in glyph sequence so that they precede (within the sequence) the base
+        * character to which they are applied. N.B. In the case of RTL segments, marks are not reordered by this,
+        * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede
+        * their base glyph.
+        *
+        * @param gdef           the glyph definition table that applies
+        * @param gs             an input glyph sequence
+        * @param unscaledWidths associated unscaled advance widths (also reordered)
+        * @param gpa            associated glyph position adjustments (also reordered)
+        * @param script         a script identifier
+        * @param language       a language identifier
+        * @return the reordered (output) glyph sequence
+        */
+       public GlyphSequence reorderCombiningMarks(GlyphDefinitionTable gdef, GlyphSequence gs, int[] unscaledWidths,
+                                                                                          int[][] gpa, String script, String language) {
+               return gs;
+       }
+
+       /**
+        * Obtain script specific required positioning features.
+        *
+        * @return array of suppported positioning features or null
+        */
+       public abstract String[] getPositioningFeatures();
+
+       /**
+        * Obtain script specific optional positioning features.
+        *
+        * @return array of suppported positioning features or null
+        */
+       public String[] getOptionalPositioningFeatures() {
+               return new String[0];
+       }
+
+       /**
+        * Obtain script specific positioning context tester.
+        *
+        * @return positioning context tester or null
+        */
+       public abstract ScriptContextTester getPositioningContextTester();
+
+       /**
+        * Perform positioning processing using a specific set of lookup tables.
+        *
+        * @param gpos        the glyph positioning table that applies
+        * @param gs          an input glyph sequence
+        * @param script      a script identifier
+        * @param language    a language identifier
+        * @param fontSize    size in device units
+        * @param lookups     a mapping from lookup specifications to glyph subtables to use for positioning processing
+        * @param widths      array of default advancements for each glyph
+        * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in
+        *                    that order,
+        *                    with one 4-tuple for each element of glyph sequence
+        * @return true if some adjustment is not zero; otherwise, false
+        */
+       public final boolean position(GlyphPositioningTable gpos, GlyphSequence gs, String script, String language,
+                                                                 int fontSize, Map/*<LookupSpec,List<LookupTable>>*/ lookups, int[] widths,
+                                                                 int[][] adjustments) {
+               return position(gs, script, language, fontSize, assembleLookups(gpos, getPositioningFeatures(), lookups), widths,
+                               adjustments, getPositioningContextTester());
+       }
+
+       /**
+        * Perform positioning processing using a specific set of ordered glyph table use specifications.
+        *
+        * @param gs          an input glyph sequence
+        * @param script      a script identifier
+        * @param language    a language identifier
+        * @param fontSize    size in device units
+        * @param usa         an ordered array of glyph table use specs
+        * @param widths      array of default advancements for each glyph in font
+        * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in
+        *                    that order,
+        *                    with one 4-tuple for each element of glyph sequence
+        * @param sct         a script specific context tester (or null)
+        * @return true if some adjustment is not zero; otherwise, false
+        */
+       public boolean position(GlyphSequence gs, String script, String language, int fontSize, GlyphTable.UseSpec[] usa,
+                                                       int[] widths, int[][] adjustments, ScriptContextTester sct) {
+               assert usa != null;
+               boolean adjusted = false;
+               for (int i = 0, n = usa.length; i < n; i++) {
+                       GlyphTable.UseSpec us = usa[i];
+                       if (us.position(gs, script, language, fontSize, widths, adjustments, sct)) {
+                               adjusted = true;
+                       }
+               }
+               return adjusted;
+       }
+
+       /**
+        * Assemble ordered array of lookup table use specifications according to the specified features and candidate
+        * lookups,
+        * where the order of the array is in accordance to the order of the applicable lookup list.
+        *
+        * @param table    the governing glyph table
+        * @param features array of feature identifiers to apply
+        * @param lookups  a mapping from lookup specifications to lists of look tables from which to select lookup tables according to
+        *                 the specified features
+        * @return ordered array of assembled lookup table use specifications
+        */
+       public final GlyphTable.UseSpec[] assembleLookups(GlyphTable table, String[] features,
+                                                                                                         Map/*<LookupSpec,List<LookupTable>>*/ lookups) {
+               AssembledLookupsKey key = new AssembledLookupsKey(table, features, lookups);
+               GlyphTable.UseSpec[] usa;
+               if ((usa = assembledLookupsGet(key)) != null) {
+                       return usa;
+               } else {
+                       return assembledLookupsPut(key, table.assembleLookups(features, lookups));
+               }
+       }
+
+       private GlyphTable.UseSpec[] assembledLookupsGet(AssembledLookupsKey key) {
+               return (GlyphTable.UseSpec[]) assembledLookups.get(key);
+       }
+
+       private GlyphTable.UseSpec[] assembledLookupsPut(AssembledLookupsKey key, GlyphTable.UseSpec[] usa) {
+               assembledLookups.put(key, usa);
+               return usa;
+       }
+
+       /**
+        * Obtain script processor instance associated with specified script.
+        *
+        * @param script a script identifier
+        * @return a script processor instance or null if none found
+        */
+       public static synchronized ScriptProcessor getInstance(String script) {
+               ScriptProcessor sp = null;
+               assert processors != null;
+               if ((sp = processors.get(script)) == null) {
+                       processors.put(script, sp = createProcessor(script));
+               }
+               return sp;
+       }
+
+       // [TBD] - rework to provide more configurable binding between script name and script processor constructor
+       private static ScriptProcessor createProcessor(String script) {
+               ScriptProcessor sp = null;
+               int sc = CharScript.scriptCodeFromTag(script);
+               if (sc == CharScript.SCRIPT_ARABIC) {
+                       sp = new ArabicScriptProcessor(script);
+               } else if (CharScript.isIndicScript(sc)) {
+                       sp = IndicScriptProcessor.makeProcessor(script);
+               } else {
+                       sp = new DefaultScriptProcessor(script);
+               }
+               return sp;
+       }
+
+       private static class AssembledLookupsKey {
+
+               private final GlyphTable table;
+               private final String[] features;
+               private final Map/*<LookupSpec,List<LookupTable>>*/ lookups;
+
+               AssembledLookupsKey(GlyphTable table, String[] features, Map/*<LookupSpec,List<LookupTable>>*/ lookups) {
+                       this.table = table;
+                       this.features = features;
+                       this.lookups = lookups;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int hashCode() {
+                       int hc = 0;
+                       hc = 7 * hc + (hc ^ table.hashCode());
+                       hc = 11 * hc + (hc ^ Arrays.hashCode(features));
+                       hc = 17 * hc + (hc ^ lookups.hashCode());
+                       return hc;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean equals(Object o) {
+                       if (o instanceof AssembledLookupsKey) {
+                               AssembledLookupsKey k = (AssembledLookupsKey) o;
+                               if (!table.equals(k.table)) {
+                                       return false;
+                               } else if (!Arrays.equals(features, k.features)) {
+                                       return false;
+                               } else {
+                                       return lookups.equals(k.lookups);
+                               }
+                       } else {
+                               return false;
+                       }
+               }
+
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/TamilScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/TamilScriptProcessor.java
new file mode 100644 (file)
index 0000000..ecb942c
--- /dev/null
@@ -0,0 +1,552 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.scripts;
+
+// CSOFF: LineLengthCheck
+
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+
+/**
+ * <p>The <code>TamilScriptProcessor</code> class implements a script processor for
+ * performing glyph substitution and positioning operations on content associated with the Tamil script.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class TamilScriptProcessor extends IndicScriptProcessor {
+
+       TamilScriptProcessor(String script) {
+               super(script);
+       }
+
+       @Override
+       protected Class<? extends TamilSyllabizer> getSyllabizerClass() {
+               return TamilSyllabizer.class;
+       }
+
+       @Override
+       // find rightmost pre-base matra
+       protected int findPreBaseMatra(GlyphSequence gs) {
+               int ng = gs.getGlyphCount();
+               int lk = -1;
+               for (int i = ng; i > 0; i--) {
+                       int k = i - 1;
+                       if (containsPreBaseMatra(gs, k)) {
+                               lk = k;
+                               break;
+                       }
+               }
+               return lk;
+       }
+
+       @Override
+       // find leftmost pre-base matra target, starting from source
+       protected int findPreBaseMatraTarget(GlyphSequence gs, int source) {
+               int ng = gs.getGlyphCount();
+               int lk = -1;
+               for (int i = (source < ng) ? source : ng; i > 0; i--) {
+                       int k = i - 1;
+                       if (containsConsonant(gs, k)) {
+                               if (containsHalfConsonant(gs, k)) {
+                                       lk = k;
+                               } else if (lk == -1) {
+                                       lk = k;
+                               } else {
+                                       break;
+                               }
+                       }
+               }
+               return lk;
+       }
+
+       private static boolean containsPreBaseMatra(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isPreM(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsConsonant(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isC(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsHalfConsonant(GlyphSequence gs, int k) {
+               Boolean half = (Boolean) gs.getAssociation(k).getPredication("half");
+               return (half != null) ? half.booleanValue() : false;
+       }
+
+       @Override
+       protected int findReph(GlyphSequence gs) {
+               int ng = gs.getGlyphCount();
+               int li = -1;
+               for (int i = 0; i < ng; i++) {
+                       if (containsReph(gs, i)) {
+                               li = i;
+                               break;
+                       }
+               }
+               return li;
+       }
+
+       @Override
+       protected int findRephTarget(GlyphSequence gs, int source) {
+               int ng = gs.getGlyphCount();
+               int c1 = -1;
+               int c2 = -1;
+               // first candidate target is after first non-half consonant
+               for (int i = 0; i < ng; i++) {
+                       if ((i != source) && containsConsonant(gs, i)) {
+                               if (!containsHalfConsonant(gs, i)) {
+                                       c1 = i + 1;
+                                       break;
+                               }
+                       }
+               }
+               // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark
+               for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) {
+                       if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) {
+                               c2 = i + 1;
+                       } else if (containsOtherMark(gs, i)) {
+                               c2 = i;
+                               break;
+                       }
+               }
+               if (c2 >= 0) {
+                       return c2;
+               } else if (c1 >= 0) {
+                       return c1;
+               } else {
+                       return source;
+               }
+       }
+
+       private static boolean containsReph(GlyphSequence gs, int k) {
+               Boolean rphf = (Boolean) gs.getAssociation(k).getPredication("rphf");
+               return (rphf != null) ? rphf.booleanValue() : false;
+       }
+
+       private static boolean containsMatra(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       if (isM(ca[i])) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private static boolean containsOtherMark(GlyphSequence gs, int k) {
+               CharAssociation a = gs.getAssociation(k);
+               int[] ca = gs.getCharacterArray(false);
+               for (int i = a.getStart(), e = a.getEnd(); i < e; i++) {
+                       switch (typeOf(ca[i])) {
+                               case C_T:   // tone (e.g., udatta, anudatta)
+                               case C_A:   // accent (e.g., acute, grave)
+                               case C_O:   // other (e.g., candrabindu, anusvara, visarga, etc)
+                                       return true;
+                               default:
+                                       break;
+                       }
+               }
+               return false;
+       }
+
+       private static class TamilSyllabizer extends DefaultSyllabizer {
+
+               TamilSyllabizer(String script, String language) {
+                       super(script, language);
+               }
+
+               @Override
+               // | C ...
+               protected int findStartOfSyllable(int[] ca, int s, int e) {
+                       if ((s < 0) || (s >= e)) {
+                               return -1;
+                       } else {
+                               while (s < e) {
+                                       int c = ca[s];
+                                       if (isC(c)) {
+                                               break;
+                                       } else {
+                                               s++;
+                                       }
+                               }
+                               return s;
+                       }
+               }
+
+               @Override
+               // D* L? | ...
+               protected int findEndOfSyllable(int[] ca, int s, int e) {
+                       if ((s < 0) || (s >= e)) {
+                               return -1;
+                       } else {
+                               int nd = 0;
+                               int nl = 0;
+                               int i;
+                               // consume dead consonants
+                               while ((i = isDeadConsonant(ca, s, e)) > s) {
+                                       s = i;
+                                       nd++;
+                               }
+                               // consume zero or one live consonant
+                               if ((i = isLiveConsonant(ca, s, e)) > s) {
+                                       s = i;
+                                       nl++;
+                               }
+                               return ((nd > 0) || (nl > 0)) ? s : -1;
+                       }
+               }
+
+               // D := ( C N? H )?
+               private int isDeadConsonant(int[] ca, int s, int e) {
+                       if (s < 0) {
+                               return -1;
+                       } else {
+                               int c;
+                               int i = 0;
+                               int nc = 0;
+                               int nh = 0;
+                               do {
+                                       // C
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isC(c)) {
+                                                       i++;
+                                                       nc++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                                       // N?
+                                       if ((s + i) < e) {
+                                               c = ca[s + 1];
+                                               if (isN(c)) {
+                                                       i++;
+                                               }
+                                       }
+                                       // H
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isH(c)) {
+                                                       i++;
+                                                       nh++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                               } while (false);
+                               return (nc > 0) && (nh > 0) ? s + i : -1;
+                       }
+               }
+
+               // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK )
+               private int isLiveConsonant(int[] ca, int s, int e) {
+                       if (s < 0) {
+                               return -1;
+                       } else {
+                               int c;
+                               int i = 0;
+                               int nc = 0;
+                               int nv = 0;
+                               int nx = 0;
+                               do {
+                                       // C
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isC(c)) {
+                                                       i++;
+                                                       nc++;
+                                               } else if (isV(c)) {
+                                                       i++;
+                                                       nv++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                                       // N?
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isN(c)) {
+                                                       i++;
+                                               }
+                                       }
+                                       // X*
+                                       while ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isX(c)) {
+                                                       i++;
+                                                       nx++;
+                                               } else {
+                                                       break;
+                                               }
+                                       }
+                               } while (false);
+                               // if no X but has H, then ignore C|I
+                               if (nx == 0) {
+                                       if ((s + i) < e) {
+                                               c = ca[s + i];
+                                               if (isH(c)) {
+                                                       if (nc > 0) {
+                                                               nc--;
+                                                       } else if (nv > 0) {
+                                                               nv--;
+                                                       }
+                                               }
+                                       }
+                               }
+                               return ((nc > 0) || (nv > 0)) ? s + i : -1;
+                       }
+               }
+       }
+
+       // tamil character types
+       static final short C_U = 0;                        // unassigned
+       static final short C_C = 1;                        // consonant
+       static final short C_V = 2;                        // vowel
+       static final short C_M = 3;                        // vowel sign (matra)
+       static final short C_S = 4;                        // symbol or sign
+       static final short C_T = 5;                        // tone mark
+       static final short C_A = 6;                        // accent mark
+       static final short C_P = 7;                        // punctuation
+       static final short C_D = 8;                        // digit
+       static final short C_H = 9;                        // halant (virama)
+       static final short C_O = 10;                       // other signs
+       static final short C_N = 0x0100;                   // nukta(ized)
+       static final short C_R = 0x0200;                   // reph(ized)
+       static final short C_PRE = 0x0400;                   // pre-base
+       static final short C_POST = 0x1000;                   // post-base
+       static final short C_WRAP = C_PRE | C_POST;           // wrap (two part) vowel
+       static final short C_M_TYPE = 0x00FF;                   // type mask
+       static final short C_M_FLAGS = 0x7F00;                   // flag mask
+       // tamil block range
+       static final int CCA_START = 0x0B80;                 // first code point mapped by cca
+       static final int CCA_END = 0x0C00;                 // last code point + 1 mapped by cca
+       // tamil character type lookups
+       static final short[] CCA = {
+                       C_U,                        // 0x0B80                   //
+                       C_U,                        // 0x0B81                   //
+                       C_O,                        // 0x0B82                   // ANUSVARA
+                       C_O,                        // 0x0B83                   // VISARGA
+                       C_U,                        // 0x0B84                   //
+                       C_V,                        // 0x0B85                   // A
+                       C_V,                        // 0x0B86                   // AA
+                       C_V,                        // 0x0B87                   // I
+                       C_V,                        // 0x0B88                   // II
+                       C_V,                        // 0x0B89                   // U
+                       C_V,                        // 0x0B8A                   // UU
+                       C_U,                        // 0x0B8B                   //
+                       C_U,                        // 0x0B8C                   //
+                       C_U,                        // 0x0B8D                   //
+                       C_V,                        // 0x0B8E                   // E
+                       C_V,                        // 0x0B8F                   // EE
+                       C_V,                        // 0x0B90                   // AI
+                       C_U,                        // 0x0B91                   //
+                       C_V,                        // 0x0B92                   // O
+                       C_V,                        // 0x0B93                   // OO
+                       C_V,                        // 0x0B94                   // AU
+                       C_C,                        // 0x0B95                   // KA
+                       C_U,                        // 0x0B96                   //
+                       C_U,                        // 0x0B97                   //
+                       C_U,                        // 0x0B98                   //
+                       C_C,                        // 0x0B99                   // NGA
+                       C_C,                        // 0x0B9A                   // CA
+                       C_U,                        // 0x0B9B                   //
+                       C_C,                        // 0x0B9C                   // JA
+                       C_U,                        // 0x0B9D                   //
+                       C_C,                        // 0x0B9E                   // NYA
+                       C_C,                        // 0x0B9F                   // TTA
+                       C_U,                        // 0x0BA0                   //
+                       C_U,                        // 0x0BA1                   //
+                       C_U,                        // 0x0BA2                   //
+                       C_C,                        // 0x0BA3                   // NNA
+                       C_C,                        // 0x0BA4                   // TA
+                       C_U,                        // 0x0BA5                   //
+                       C_U,                        // 0x0BA6                   //
+                       C_U,                        // 0x0BA7                   //
+                       C_C,                        // 0x0BA8                   // NA
+                       C_C,                        // 0x0BA9                   // NNNA
+                       C_C,                        // 0x0BAA                   // PA
+                       C_U,                        // 0x0BAB                   //
+                       C_U,                        // 0x0BAC                   //
+                       C_U,                        // 0x0BAD                   //
+                       C_C,                        // 0x0BAE                   // MA
+                       C_C,                        // 0x0BAF                   // YA
+                       C_C | C_R,                  // 0x0BB0                   // RA
+                       C_C | C_R,                  // 0x0BB1                   // RRA
+                       C_C,                        // 0x0BB2                   // LA
+                       C_C,                        // 0x0BB3                   // LLA
+                       C_C,                        // 0x0BB4                   // LLLA
+                       C_C,                        // 0x0BB5                   // VA
+                       C_C,                        // 0x0BB6                   // SHA
+                       C_C,                        // 0x0BB7                   // SSA
+                       C_C,                        // 0x0BB8                   // SA
+                       C_C,                        // 0x0BB9                   // HA
+                       C_U,                        // 0x0BBA                   //
+                       C_U,                        // 0x0BBB                   //
+                       C_U,                        // 0x0BBC                   //
+                       C_U,                        // 0x0BBD                   //
+                       C_M,                        // 0x0BBE                   // AA
+                       C_M,                        // 0x0BBF                   // I
+                       C_M,                        // 0x0BC0                   // II
+                       C_M,                        // 0x0BC1                   // U
+                       C_M,                        // 0x0BC2                   // UU
+                       C_U,                        // 0x0BC3                   //
+                       C_U,                        // 0x0BC4                   //
+                       C_U,                        // 0x0BC5                   //
+                       C_M | C_PRE,                // 0x0BC6                   // E
+                       C_M | C_PRE,                // 0x0BC7                   // EE
+                       C_M | C_PRE,                // 0x0BC8                   // AI
+                       C_U,                        // 0x0BC9                   //
+                       C_M | C_WRAP,               // 0x0BCA                   // O
+                       C_M | C_WRAP,               // 0x0BCB                   // OO
+                       C_M | C_WRAP,               // 0x0BCC                   // AU
+                       C_H,                        // 0x0BCD                   // VIRAMA (HALANT)
+                       C_U,                        // 0x0BCE                   //
+                       C_U,                        // 0x0BCF                   //
+                       C_S,                        // 0x0BD0                   // OM
+                       C_U,                        // 0x0BD1                   //
+                       C_U,                        // 0x0BD2                   //
+                       C_U,                        // 0x0BD3                   //
+                       C_U,                        // 0x0BD4                   //
+                       C_U,                        // 0x0BD5                   //
+                       C_U,                        // 0x0BD6                   //
+                       C_M,                        // 0x0BD7                   // AU LENGTH MARK
+                       C_U,                        // 0x0BD8                   //
+                       C_U,                        // 0x0BD9                   //
+                       C_U,                        // 0x0BDA                   //
+                       C_U,                        // 0x0BDB                   //
+                       C_U,                        // 0x0BDC                   //
+                       C_U,                        // 0x0BDD                   //
+                       C_U,                        // 0x0BDE                   //
+                       C_U,                        // 0x0BDF                   //
+                       C_U,                        // 0x0BE0                   //
+                       C_U,                        // 0x0BE1                   //
+                       C_U,                        // 0x0BE2                   //
+                       C_U,                        // 0x0BE3                   //
+                       C_U,                        // 0x0BE4                   //
+                       C_U,                        // 0x0BE5                   //
+                       C_D,                        // 0x0BE6                   // ZERO
+                       C_D,                        // 0x0BE7                   // ONE
+                       C_D,                        // 0x0BE8                   // TWO
+                       C_D,                        // 0x0BE9                   // THREE
+                       C_D,                        // 0x0BEA                   // FOUR
+                       C_D,                        // 0x0BEB                   // FIVE
+                       C_D,                        // 0x0BEC                   // SIX
+                       C_D,                        // 0x0BED                   // SEVEN
+                       C_D,                        // 0x0BEE                   // EIGHT
+                       C_D,                        // 0x0BEF                   // NINE
+                       C_S,                        // 0x0BF0                   // TEN
+                       C_S,                        // 0x0BF1                   // ONE HUNDRED
+                       C_S,                        // 0x0BF2                   // ONE THOUSAND
+                       C_S,                        // 0x0BF3                   // DAY SIGN (naal)
+                       C_S,                        // 0x0BF4                   // MONTH SIGN (maatham)
+                       C_S,                        // 0x0BF5                   // YEAR SIGN (varudam)
+                       C_S,                        // 0x0BF6                   // DEBIT SIGN (patru)
+                       C_S,                        // 0x0BF7                   // CREDIT SIGN (varavu)
+                       C_S,                        // 0x0BF8                   // AS ABOVE SIGN (merpadi)
+                       C_S,                        // 0x0BF9                   // RUPEE SIGN (rupai)
+                       C_S,                        // 0x0BFA                   // NUMBER SIGN (enn)
+                       C_U,                        // 0x0BFB                   //
+                       C_U,                        // 0x0BFC                   //
+                       C_U,                        // 0x0BFD                   //
+                       C_U,                        // 0x0BFE                   //
+                       C_U                         // 0x0BFF                   //
+       };
+
+       static int typeOf(int c) {
+               if ((c >= CCA_START) && (c < CCA_END)) {
+                       return CCA[c - CCA_START] & C_M_TYPE;
+               } else {
+                       return C_U;
+               }
+       }
+
+       static boolean isType(int c, int t) {
+               return typeOf(c) == t;
+       }
+
+       static boolean hasFlag(int c, int f) {
+               if ((c >= CCA_START) && (c < CCA_END)) {
+                       return (CCA[c - CCA_START] & f) == f;
+               } else {
+                       return false;
+               }
+       }
+
+       static boolean isC(int c) {
+               return isType(c, C_C);
+       }
+
+       static boolean isR(int c) {
+               return isType(c, C_C) && hasR(c);
+       }
+
+       static boolean isV(int c) {
+               return isType(c, C_V);
+       }
+
+       static boolean isN(int c) {
+               return c == 0x093C;
+       }
+
+       static boolean isH(int c) {
+               return c == 0x094D;
+       }
+
+       static boolean isM(int c) {
+               return isType(c, C_M);
+       }
+
+       static boolean isPreM(int c) {
+               return isType(c, C_M) && hasFlag(c, C_PRE);
+       }
+
+       static boolean isX(int c) {
+               switch (typeOf(c)) {
+                       case C_M: // matra (combining vowel)
+                       case C_A: // accent mark
+                       case C_T: // tone mark
+                       case C_O: // other (modifying) mark
+                               return true;
+                       default:
+                               return false;
+               }
+       }
+
+       static boolean hasR(int c) {
+               return hasFlag(c, C_R);
+       }
+
+       static boolean hasN(int c) {
+               return hasFlag(c, C_N);
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharAssociation.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharAssociation.java
new file mode 100644 (file)
index 0000000..1047fe0
--- /dev/null
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A structure class encapsulating an interval of characters expressed as an offset and count of
+ * Unicode scalar values (in an IntBuffer). A <code>CharAssociation</code> is used to maintain a
+ * backpointer from a glyph to one or more character intervals from which the glyph was derived.
+ * <p>
+ * Each glyph in a glyph sequence is associated with a single <code>CharAssociation</code> instance.
+ * <p>
+ * A <code>CharAssociation</code> instance is additionally (and optionally) used to record
+ * predication information about the glyph, such as whether the glyph was produced by the
+ * application of a specific substitution table or whether its position was adjusted by a specific
+ * poisitioning table.
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class CharAssociation implements Cloneable {
+
+       // instance state
+       private final int offset;
+       private final int count;
+       private final int[] subIntervals;
+       private Map<String, Object> predications;
+
+       // class state
+       private static volatile Map<String, PredicationMerger> predicationMergers;
+
+       interface PredicationMerger {
+
+               Object merge(String key, Object v1, Object v2);
+       }
+
+       /**
+        * Instantiate a character association.
+        *
+        * @param offset       into array of Unicode scalar values (in associated IntBuffer)
+        * @param count        of Unicode scalar values (in associated IntBuffer)
+        * @param subIntervals if disjoint, then array of sub-intervals, otherwise null; even
+        *                     members of array are sub-interval starts, and odd members are sub-interval
+        *                     ends (exclusive)
+        */
+       public CharAssociation(int offset, int count, int[] subIntervals) {
+               this.offset = offset;
+               this.count = count;
+               this.subIntervals = ((subIntervals != null) && (subIntervals.length > 2)) ? subIntervals : null;
+       }
+
+       /**
+        * Instantiate a non-disjoint character association.
+        *
+        * @param offset into array of UTF-16 code elements (in associated CharSequence)
+        * @param count  of UTF-16 character code elements (in associated CharSequence)
+        */
+       public CharAssociation(int offset, int count) {
+               this(offset, count, null);
+       }
+
+       /**
+        * Instantiate a non-disjoint character association.
+        *
+        * @param subIntervals if disjoint, then array of sub-intervals, otherwise null; even
+        *                     members of array are sub-interval starts, and odd members are sub-interval
+        *                     ends (exclusive)
+        */
+       public CharAssociation(int[] subIntervals) {
+               this(getSubIntervalsStart(subIntervals), getSubIntervalsLength(subIntervals), subIntervals);
+       }
+
+       /**
+        * @return offset (start of association interval)
+        */
+       public int getOffset() {
+               return offset;
+       }
+
+       /**
+        * @return count (number of characer codes in association)
+        */
+       public int getCount() {
+               return count;
+       }
+
+       /**
+        * @return start of association interval
+        */
+       public int getStart() {
+               return getOffset();
+       }
+
+       /**
+        * @return end of association interval
+        */
+       public int getEnd() {
+               return getOffset() + getCount();
+       }
+
+       /**
+        * @return true if association is disjoint
+        */
+       public boolean isDisjoint() {
+               return subIntervals != null;
+       }
+
+       /**
+        * @return subintervals of disjoint association
+        */
+       public int[] getSubIntervals() {
+               return subIntervals;
+       }
+
+       /**
+        * @return count of subintervals of disjoint association
+        */
+       public int getSubIntervalCount() {
+               return (subIntervals != null) ? (subIntervals.length / 2) : 0;
+       }
+
+       /**
+        * @param offset of interval in sequence
+        * @param count  length of interval
+        * @return true if this association is contained within [offset,offset+count)
+        */
+       public boolean contained(int offset, int count) {
+               int s = offset;
+               int e = offset + count;
+               if (!isDisjoint()) {
+                       int s0 = getStart();
+                       int e0 = getEnd();
+                       return (s0 >= s) && (e0 <= e);
+               } else {
+                       int ns = getSubIntervalCount();
+                       for (int i = 0; i < ns; i++) {
+                               int s0 = subIntervals[2 * i + 0];
+                               int e0 = subIntervals[2 * i + 1];
+                               if ((s0 >= s) && (e0 <= e)) {
+                                       return true;
+                               }
+                       }
+                       return false;
+               }
+       }
+
+       /**
+        * Set predication <KEY,VALUE>.
+        *
+        * @param key   predication key
+        * @param value predication value
+        */
+       public void setPredication(String key, Object value) {
+               if (predications == null) {
+                       predications = new HashMap<String, Object>();
+               }
+               if (predications != null) {
+                       predications.put(key, value);
+               }
+       }
+
+       /**
+        * Get predication KEY.
+        *
+        * @param key predication key
+        * @return predication KEY at OFFSET or null if none exists
+        */
+       public Object getPredication(String key) {
+               if (predications != null) {
+                       return predications.get(key);
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Merge predication <KEY,VALUE>.
+        *
+        * @param key   predication key
+        * @param value predication value
+        */
+       public void mergePredication(String key, Object value) {
+               if (predications == null) {
+                       predications = new HashMap<String, Object>();
+               }
+               if (predications != null) {
+                       if (predications.containsKey(key)) {
+                               Object v1 = predications.get(key);
+                               Object v2 = value;
+                               predications.put(key, mergePredicationValues(key, v1, v2));
+                       } else {
+                               predications.put(key, value);
+                       }
+               }
+       }
+
+       /**
+        * Merge predication values V1 and V2 on KEY. Uses registered <code>PredicationMerger</code>
+        * if one exists, otherwise uses V2 if non-null, otherwise uses V1.
+        *
+        * @param key predication key
+        * @param v1  first (original) predication value
+        * @param v2  second (to be merged) predication value
+        * @return merged value
+        */
+       public static Object mergePredicationValues(String key, Object v1, Object v2) {
+               PredicationMerger pm = getPredicationMerger(key);
+               if (pm != null) {
+                       return pm.merge(key, v1, v2);
+               } else if (v2 != null) {
+                       return v2;
+               } else {
+                       return v1;
+               }
+       }
+
+       /**
+        * Merge predications from another CA.
+        *
+        * @param ca from which to merge
+        */
+       public void mergePredications(CharAssociation ca) {
+               if (ca.predications != null) {
+                       for (Map.Entry<String, Object> e : ca.predications.entrySet()) {
+                               mergePredication(e.getKey(), e.getValue());
+                       }
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public Object clone() {
+               try {
+                       CharAssociation ca = (CharAssociation) super.clone();
+                       if (predications != null) {
+                               ca.predications = new HashMap<String, Object>(predications);
+                       }
+                       return ca;
+               } catch (CloneNotSupportedException e) {
+                       return null;
+               }
+       }
+
+       /**
+        * Register predication merger PM for KEY.
+        *
+        * @param key for predication merger
+        * @param pm  predication merger
+        */
+       public static void setPredicationMerger(String key, PredicationMerger pm) {
+               if (predicationMergers == null) {
+                       predicationMergers = new HashMap<String, PredicationMerger>();
+               }
+               if (predicationMergers != null) {
+                       predicationMergers.put(key, pm);
+               }
+       }
+
+       /**
+        * Obtain predication merger for KEY.
+        *
+        * @param key for predication merger
+        * @return predication merger or null if none exists
+        */
+       public static PredicationMerger getPredicationMerger(String key) {
+               if (predicationMergers != null) {
+                       return predicationMergers.get(key);
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Replicate association to form <code>repeat</code> new associations.
+        *
+        * @param a      association to replicate
+        * @param repeat count
+        * @return array of replicated associations
+        */
+       public static CharAssociation[] replicate(CharAssociation a, int repeat) {
+               CharAssociation[] aa = new CharAssociation[repeat];
+               for (int i = 0, n = aa.length; i < n; i++) {
+                       aa[i] = (CharAssociation) a.clone();
+               }
+               return aa;
+       }
+
+       /**
+        * Join (merge) multiple associations into a single, potentially disjoint
+        * association.
+        *
+        * @param aa array of associations to join
+        * @return (possibly disjoint) association containing joined associations
+        */
+       public static CharAssociation join(CharAssociation[] aa) {
+               CharAssociation ca;
+               // extract sorted intervals
+               int[] ia = extractIntervals(aa);
+               if ((ia == null) || (ia.length == 0)) {
+                       ca = new CharAssociation(0, 0);
+               } else if (ia.length == 2) {
+                       int s = ia[0];
+                       int e = ia[1];
+                       ca = new CharAssociation(s, e - s);
+               } else {
+                       ca = new CharAssociation(mergeIntervals(ia));
+               }
+               return mergePredicates(ca, aa);
+       }
+
+       private static CharAssociation mergePredicates(CharAssociation ca, CharAssociation[] aa) {
+               for (CharAssociation a : aa) {
+                       ca.mergePredications(a);
+               }
+               return ca;
+       }
+
+       private static int getSubIntervalsStart(int[] ia) {
+               int us = Integer.MAX_VALUE;
+               int ue = Integer.MIN_VALUE;
+               if (ia != null) {
+                       for (int i = 0, n = ia.length; i < n; i += 2) {
+                               int s = ia[i + 0];
+                               int e = ia[i + 1];
+                               if (s < us) {
+                                       us = s;
+                               }
+                               if (e > ue) {
+                                       ue = e;
+                               }
+                       }
+                       if (ue < 0) {
+                               ue = 0;
+                       }
+                       if (us > ue) {
+                               us = ue;
+                       }
+               }
+               return us;
+       }
+
+       private static int getSubIntervalsLength(int[] ia) {
+               int us = Integer.MAX_VALUE;
+               int ue = Integer.MIN_VALUE;
+               if (ia != null) {
+                       for (int i = 0, n = ia.length; i < n; i += 2) {
+                               int s = ia[i + 0];
+                               int e = ia[i + 1];
+                               if (s < us) {
+                                       us = s;
+                               }
+                               if (e > ue) {
+                                       ue = e;
+                               }
+                       }
+                       if (ue < 0) {
+                               ue = 0;
+                       }
+                       if (us > ue) {
+                               us = ue;
+                       }
+               }
+               return ue - us;
+       }
+
+       /**
+        * Extract sorted sub-intervals.
+        */
+       private static int[] extractIntervals(CharAssociation[] aa) {
+               int ni = 0;
+               for (int i = 0, n = aa.length; i < n; i++) {
+                       CharAssociation a = aa[i];
+                       if (a.isDisjoint()) {
+                               ni += a.getSubIntervalCount();
+                       } else {
+                               ni += 1;
+                       }
+               }
+               int[] sa = new int[ni];
+               int[] ea = new int[ni];
+               for (int i = 0, k = 0; i < aa.length; i++) {
+                       CharAssociation a = aa[i];
+                       if (a.isDisjoint()) {
+                               int[] da = a.getSubIntervals();
+                               for (int j = 0; j < da.length; j += 2) {
+                                       sa[k] = da[j + 0];
+                                       ea[k] = da[j + 1];
+                                       k++;
+                               }
+                       } else {
+                               sa[k] = a.getStart();
+                               ea[k] = a.getEnd();
+                               k++;
+                       }
+               }
+               return sortIntervals(sa, ea);
+       }
+
+       private static final int[] SORT_INCREMENTS_16
+                       = {1391376, 463792, 198768, 86961, 33936, 13776, 4592, 1968, 861, 336, 112, 48, 21, 7, 3, 1};
+
+       private static final int[] SORT_INCREMENTS_03
+                       = {7, 3, 1};
+
+       /**
+        * Sort sub-intervals using modified Shell Sort.
+        */
+       private static int[] sortIntervals(int[] sa, int[] ea) {
+               assert sa != null;
+               assert ea != null;
+               assert sa.length == ea.length;
+               int ni = sa.length;
+               int[] incr = (ni < 21) ? SORT_INCREMENTS_03 : SORT_INCREMENTS_16;
+               for (int k = 0; k < incr.length; k++) {
+                       for (int h = incr[k], i = h, n = ni, j; i < n; i++) {
+                               int s1 = sa[i];
+                               int e1 = ea[i];
+                               for (j = i; j >= h; j -= h) {
+                                       int s2 = sa[j - h];
+                                       int e2 = ea[j - h];
+                                       if (s2 > s1) {
+                                               sa[j] = s2;
+                                               ea[j] = e2;
+                                       } else if ((s2 == s1) && (e2 > e1)) {
+                                               sa[j] = s2;
+                                               ea[j] = e2;
+                                       } else {
+                                               break;
+                                       }
+                               }
+                               sa[j] = s1;
+                               ea[j] = e1;
+                       }
+               }
+               int[] ia = new int[ni * 2];
+               for (int i = 0; i < ni; i++) {
+                       ia[(i * 2) + 0] = sa[i];
+                       ia[(i * 2) + 1] = ea[i];
+               }
+               return ia;
+       }
+
+       /**
+        * Merge overlapping and abutting sub-intervals.
+        */
+       private static int[] mergeIntervals(int[] ia) {
+               int ni = ia.length;
+               int i;
+               int n;
+               int nm;
+               int is;
+               int ie;
+               // count merged sub-intervals
+               for (i = 0, n = ni, nm = 0, is = ie = -1; i < n; i += 2) {
+                       int s = ia[i + 0];
+                       int e = ia[i + 1];
+                       if ((ie < 0) || (s > ie)) {
+                               is = s;
+                               ie = e;
+                               nm++;
+                       } else if (s >= is) {
+                               if (e > ie) {
+                                       ie = e;
+                               }
+                       }
+               }
+               int[] mi = new int[nm * 2];
+               // populate merged sub-intervals
+               for (i = 0, n = ni, nm = 0, is = ie = -1; i < n; i += 2) {
+                       int s = ia[i + 0];
+                       int e = ia[i + 1];
+                       int k = nm * 2;
+                       if ((ie < 0) || (s > ie)) {
+                               is = s;
+                               ie = e;
+                               mi[k + 0] = is;
+                               mi[k + 1] = ie;
+                               nm++;
+                       } else if (s >= is) {
+                               if (e > ie) {
+                                       ie = e;
+                               }
+                               mi[k - 1] = ie;
+                       }
+               }
+               return mi;
+       }
+
+       @Override
+       public String toString() {
+               StringBuffer sb = new StringBuffer();
+               sb.append('[');
+               sb.append(offset);
+               sb.append(',');
+               sb.append(count);
+               sb.append(']');
+               return sb.toString();
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharScript.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharScript.java
new file mode 100644 (file)
index 0000000..4994be8
--- /dev/null
@@ -0,0 +1,955 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.complexscripts.util;
+
+import com.jaredrummler.fontreader.util.CharUtilities;
+
+import java.util.*;
+
+/**
+ * <p>Script related utilities.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public final class CharScript {
+
+       // CSOFF: LineLength
+
+       //
+       // The following script codes are based on ISO 15924. Codes less than 1000 are
+       // official assignments from 15924; those equal to or greater than 1000 are FOP
+       // implementation specific.
+       //
+       /**
+        * hebrew script constant
+        */
+       public static final int SCRIPT_HEBREW = 125;  // 'hebr'
+       /**
+        * mongolian script constant
+        */
+       public static final int SCRIPT_MONGOLIAN = 145;  // 'mong'
+       /**
+        * arabic script constant
+        */
+       public static final int SCRIPT_ARABIC = 160;  // 'arab'
+       /**
+        * greek script constant
+        */
+       public static final int SCRIPT_GREEK = 200;  // 'grek'
+       /**
+        * latin script constant
+        */
+       public static final int SCRIPT_LATIN = 215;  // 'latn'
+       /**
+        * cyrillic script constant
+        */
+       public static final int SCRIPT_CYRILLIC = 220;  // 'cyrl'
+       /**
+        * georgian script constant
+        */
+       public static final int SCRIPT_GEORGIAN = 240;  // 'geor'
+       /**
+        * bopomofo script constant
+        */
+       public static final int SCRIPT_BOPOMOFO = 285;  // 'bopo'
+       /**
+        * hangul script constant
+        */
+       public static final int SCRIPT_HANGUL = 286;  // 'hang'
+       /**
+        * gurmukhi script constant
+        */
+       public static final int SCRIPT_GURMUKHI = 310;  // 'guru'
+       /**
+        * gurmukhi 2 script constant
+        */
+       public static final int SCRIPT_GURMUKHI_2 = 1310;
+       // 'gur2'       -- MSFT (pseudo) script tag for variant shaping semantics
+       /**
+        * devanagari script constant
+        */
+       public static final int SCRIPT_DEVANAGARI = 315;  // 'deva'
+       /**
+        * devanagari 2 script constant
+        */
+       public static final int SCRIPT_DEVANAGARI_2 = 1315;
+       // 'dev2'       -- MSFT (pseudo) script tag for variant shaping semantics
+       /**
+        * gujarati script constant
+        */
+       public static final int SCRIPT_GUJARATI = 320;  // 'gujr'
+       /**
+        * gujarati 2 script constant
+        */
+       public static final int SCRIPT_GUJARATI_2 = 1320;
+       // 'gjr2'       -- MSFT (pseudo) script tag for variant shaping semantics
+       /**
+        * bengali script constant
+        */
+       public static final int SCRIPT_BENGALI = 326;  // 'beng'
+       /**
+        * bengali 2 script constant
+        */
+       public static final int SCRIPT_BENGALI_2 = 1326;
+       // 'bng2'       -- MSFT (pseudo) script tag for variant shaping semantics
+       /**
+        * oriya script constant
+        */
+       public static final int SCRIPT_ORIYA = 327;  // 'orya'
+       /**
+        * oriya 2 script constant
+        */
+       public static final int SCRIPT_ORIYA_2 = 1327;
+       // 'ory2'       -- MSFT (pseudo) script tag for variant shaping semantics
+       /**
+        * tibetan script constant
+        */
+       public static final int SCRIPT_TIBETAN = 330;  // 'tibt'
+       /**
+        * telugu script constant
+        */
+       public static final int SCRIPT_TELUGU = 340;  // 'telu'
+       /**
+        * telugu 2 script constant
+        */
+       public static final int SCRIPT_TELUGU_2 = 1340;
+       // 'tel2'       -- MSFT (pseudo) script tag for variant shaping semantics
+       /**
+        * kannada script constant
+        */
+       public static final int SCRIPT_KANNADA = 345;  // 'knda'
+       /**
+        * kannada 2 script constant
+        */
+       public static final int SCRIPT_KANNADA_2 = 1345;
+       // 'knd2'       -- MSFT (pseudo) script tag for variant shaping semantics
+       /**
+        * tamil script constant
+        */
+       public static final int SCRIPT_TAMIL = 346;  // 'taml'
+       /**
+        * tamil 2 script constant
+        */
+       public static final int SCRIPT_TAMIL_2 = 1346;
+       // 'tml2'       -- MSFT (pseudo) script tag for variant shaping semantics
+       /**
+        * malayalam script constant
+        */
+       public static final int SCRIPT_MALAYALAM = 347;  // 'mlym'
+       /**
+        * malayalam 2 script constant
+        */
+       public static final int SCRIPT_MALAYALAM_2 = 1347;
+       // 'mlm2'       -- MSFT (pseudo) script tag for variant shaping semantics
+       /**
+        * sinhalese script constant
+        */
+       public static final int SCRIPT_SINHALESE = 348;  // 'sinh'
+       /**
+        * burmese script constant
+        */
+       public static final int SCRIPT_BURMESE = 350;  // 'mymr'
+       /**
+        * thai script constant
+        */
+       public static final int SCRIPT_THAI = 352;  // 'thai'
+       /**
+        * khmer script constant
+        */
+       public static final int SCRIPT_KHMER = 355;  // 'khmr'
+       /**
+        * lao script constant
+        */
+       public static final int SCRIPT_LAO = 356;  // 'laoo'
+       /**
+        * hiragana script constant
+        */
+       public static final int SCRIPT_HIRAGANA = 410;  // 'hira'
+       /**
+        * ethiopic script constant
+        */
+       public static final int SCRIPT_ETHIOPIC = 430;  // 'ethi'
+       /**
+        * han script constant
+        */
+       public static final int SCRIPT_HAN = 500;  // 'hani'
+       /**
+        * katakana script constant
+        */
+       public static final int SCRIPT_KATAKANA = 410;  // 'kana'
+       /**
+        * math script constant
+        */
+       public static final int SCRIPT_MATH = 995;  // 'zmth'
+       /**
+        * symbol script constant
+        */
+       public static final int SCRIPT_SYMBOL = 996;  // 'zsym'
+       /**
+        * undetermined script constant
+        */
+       public static final int SCRIPT_UNDETERMINED = 998;  // 'zyyy'
+       /**
+        * uncoded script constant
+        */
+       public static final int SCRIPT_UNCODED = 999;  // 'zzzz'
+
+       /**
+        * A static (class) parameter indicating whether V2 indic shaping
+        * rules apply or not, with default being <code>true</code>.
+        */
+       private static final boolean USE_V2_INDIC = true;
+
+       private CharScript() {
+       }
+
+       /**
+        * Determine if character c is punctuation.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character is punctuation
+        */
+       public static boolean isPunctuation(int c) {
+               if ((c >= 0x0021) && (c <= 0x002F)) {             // basic latin punctuation
+                       return true;
+               } else if ((c >= 0x003A) && (c <= 0x0040)) {      // basic latin punctuation
+                       return true;
+               } else if ((c >= 0x005F) && (c <= 0x0060)) {      // basic latin punctuation
+                       return true;
+               } else if ((c >= 0x007E) && (c <= 0x007E)) {      // basic latin punctuation
+                       return true;
+               } else if ((c >= 0x00A1) && (c <= 0x00BF)) {      // latin supplement punctuation
+                       return true;
+               } else if ((c >= 0x00D7) && (c <= 0x00D7)) {      // latin supplement punctuation
+                       return true;
+               } else if ((c >= 0x00F7) && (c <= 0x00F7)) {      // latin supplement punctuation
+                       return true;
+               } else // general punctuation
+// [TBD] - not complete
+                       return (c >= 0x2000) && (c <= 0x206F);
+       }
+
+       /**
+        * Determine if character c is a digit.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character is a digit
+        */
+       public static boolean isDigit(int c) {
+               // basic latin digits
+// [TBD] - not complete
+               return (c >= 0x0030) && (c <= 0x0039);
+       }
+
+       /**
+        * Determine if character c belong to the hebrew script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to hebrew script
+        */
+       public static boolean isHebrew(int c) {
+               if ((c >= 0x0590) && (c <= 0x05FF)) {             // hebrew block
+                       return true;
+               } else // hebrew presentation forms block
+                       return (c >= 0xFB00) && (c <= 0xFB4F);
+       }
+
+       /**
+        * Determine if character c belong to the mongolian script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to mongolian script
+        */
+       public static boolean isMongolian(int c) {
+               // mongolian block
+               return (c >= 0x1800) && (c <= 0x18AF);
+       }
+
+       /**
+        * Determine if character c belong to the arabic script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to arabic script
+        */
+       public static boolean isArabic(int c) {
+               if ((c >= 0x0600) && (c <= 0x06FF)) {             // arabic block
+                       return true;
+               } else if ((c >= 0x0750) && (c <= 0x077F)) {      // arabic supplement block
+                       return true;
+               } else if ((c >= 0xFB50) && (c <= 0xFDFF)) {      // arabic presentation forms a block
+                       return true;
+               } else // arabic presentation forms b block
+                       return (c >= 0xFE70) && (c <= 0xFEFF);
+       }
+
+       /**
+        * Determine if character c belong to the greek script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to greek script
+        */
+       public static boolean isGreek(int c) {
+               if ((c >= 0x0370) && (c <= 0x03FF)) {             // greek (and coptic) block
+                       return true;
+               } else // greek extended block
+                       return (c >= 0x1F00) && (c <= 0x1FFF);
+       }
+
+       /**
+        * Determine if character c belong to the latin script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to latin script
+        */
+       public static boolean isLatin(int c) {
+               if ((c >= 0x0041) && (c <= 0x005A)) {             // basic latin upper case
+                       return true;
+               } else if ((c >= 0x0061) && (c <= 0x007A)) {      // basic latin lower case
+                       return true;
+               } else if ((c >= 0x00C0) && (c <= 0x00D6)) {      // latin supplement upper case
+                       return true;
+               } else if ((c >= 0x00D8) && (c <= 0x00DF)) {      // latin supplement upper case
+                       return true;
+               } else if ((c >= 0x00E0) && (c <= 0x00F6)) {      // latin supplement lower case
+                       return true;
+               } else if ((c >= 0x00F8) && (c <= 0x00FF)) {      // latin supplement lower case
+                       return true;
+               } else if ((c >= 0x0100) && (c <= 0x017F)) {      // latin extended a
+                       return true;
+               } else if ((c >= 0x0180) && (c <= 0x024F)) {      // latin extended b
+                       return true;
+               } else if ((c >= 0x1E00) && (c <= 0x1EFF)) {      // latin extended additional
+                       return true;
+               } else if ((c >= 0x2C60) && (c <= 0x2C7F)) {      // latin extended c
+                       return true;
+               } else if ((c >= 0xA720) && (c <= 0xA7FF)) {      // latin extended d
+                       return true;
+               } else // latin ligatures
+                       return (c >= 0xFB00) && (c <= 0xFB0F);
+       }
+
+       /**
+        * Determine if character c belong to the cyrillic script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to cyrillic script
+        */
+       public static boolean isCyrillic(int c) {
+               if ((c >= 0x0400) && (c <= 0x04FF)) {             // cyrillic block
+                       return true;
+               } else if ((c >= 0x0500) && (c <= 0x052F)) {      // cyrillic supplement block
+                       return true;
+               } else if ((c >= 0x2DE0) && (c <= 0x2DFF)) {      // cyrillic extended-a block
+                       return true;
+               } else // cyrillic extended-b block
+                       return (c >= 0xA640) && (c <= 0xA69F);
+       }
+
+       /**
+        * Determine if character c belong to the georgian script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to georgian script
+        */
+       public static boolean isGeorgian(int c) {
+               if ((c >= 0x10A0) && (c <= 0x10FF)) {             // georgian block
+                       return true;
+               } else // georgian supplement block
+                       return (c >= 0x2D00) && (c <= 0x2D2F);
+       }
+
+       /**
+        * Determine if character c belong to the hangul script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to hangul script
+        */
+       public static boolean isHangul(int c) {
+               if ((c >= 0x1100) && (c <= 0x11FF)) {             // hangul jamo
+                       return true;
+               } else if ((c >= 0x3130) && (c <= 0x318F)) {      // hangul compatibility jamo
+                       return true;
+               } else if ((c >= 0xA960) && (c <= 0xA97F)) {      // hangul jamo extended a
+                       return true;
+               } else if ((c >= 0xAC00) && (c <= 0xD7A3)) {      // hangul syllables
+                       return true;
+               } else // hangul jamo extended a
+                       return (c >= 0xD7B0) && (c <= 0xD7FF);
+       }
+
+       /**
+        * Determine if character c belong to the gurmukhi script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to gurmukhi script
+        */
+       public static boolean isGurmukhi(int c) {
+               // gurmukhi block
+               return (c >= 0x0A00) && (c <= 0x0A7F);
+       }
+
+       /**
+        * Determine if character c belong to the devanagari script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to devanagari script
+        */
+       public static boolean isDevanagari(int c) {
+               if ((c >= 0x0900) && (c <= 0x097F)) {             // devangari block
+                       return true;
+               } else // devangari extended block
+                       return (c >= 0xA8E0) && (c <= 0xA8FF);
+       }
+
+       /**
+        * Determine if character c belong to the gujarati script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to gujarati script
+        */
+       public static boolean isGujarati(int c) {
+               // gujarati block
+               return (c >= 0x0A80) && (c <= 0x0AFF);
+       }
+
+       /**
+        * Determine if character c belong to the bengali script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to bengali script
+        */
+       public static boolean isBengali(int c) {
+               // bengali block
+               return (c >= 0x0980) && (c <= 0x09FF);
+       }
+
+       /**
+        * Determine if character c belong to the oriya script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to oriya script
+        */
+       public static boolean isOriya(int c) {
+               // oriya block
+               return (c >= 0x0B00) && (c <= 0x0B7F);
+       }
+
+       /**
+        * Determine if character c belong to the tibetan script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to tibetan script
+        */
+       public static boolean isTibetan(int c) {
+               // tibetan block
+               return (c >= 0x0F00) && (c <= 0x0FFF);
+       }
+
+       /**
+        * Determine if character c belong to the telugu script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to telugu script
+        */
+       public static boolean isTelugu(int c) {
+               // telugu block
+               return (c >= 0x0C00) && (c <= 0x0C7F);
+       }
+
+       /**
+        * Determine if character c belong to the kannada script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to kannada script
+        */
+       public static boolean isKannada(int c) {
+               // kannada block
+               return (c >= 0x0C00) && (c <= 0x0C7F);
+       }
+
+       /**
+        * Determine if character c belong to the tamil script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to tamil script
+        */
+       public static boolean isTamil(int c) {
+               // tamil block
+               return (c >= 0x0B80) && (c <= 0x0BFF);
+       }
+
+       /**
+        * Determine if character c belong to the malayalam script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to malayalam script
+        */
+       public static boolean isMalayalam(int c) {
+               // malayalam block
+               return (c >= 0x0D00) && (c <= 0x0D7F);
+       }
+
+       /**
+        * Determine if character c belong to the sinhalese script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to sinhalese script
+        */
+       public static boolean isSinhalese(int c) {
+               // sinhala block
+               return (c >= 0x0D80) && (c <= 0x0DFF);
+       }
+
+       /**
+        * Determine if character c belong to the burmese script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to burmese script
+        */
+       public static boolean isBurmese(int c) {
+               if ((c >= 0x1000) && (c <= 0x109F)) {             // burmese (myanmar) block
+                       return true;
+               } else // burmese (myanmar) extended block
+                       return (c >= 0xAA60) && (c <= 0xAA7F);
+       }
+
+       /**
+        * Determine if character c belong to the thai script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to thai script
+        */
+       public static boolean isThai(int c) {
+               // thai block
+               return (c >= 0x0E00) && (c <= 0x0E7F);
+       }
+
+       /**
+        * Determine if character c belong to the khmer script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to khmer script
+        */
+       public static boolean isKhmer(int c) {
+               if ((c >= 0x1780) && (c <= 0x17FF)) {             // khmer block
+                       return true;
+               } else // khmer symbols block
+                       return (c >= 0x19E0) && (c <= 0x19FF);
+       }
+
+       /**
+        * Determine if character c belong to the lao script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to lao script
+        */
+       public static boolean isLao(int c) {
+               // lao block
+               return (c >= 0x0E80) && (c <= 0x0EFF);
+       }
+
+       /**
+        * Determine if character c belong to the ethiopic (amharic) script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to ethiopic (amharic) script
+        */
+       public static boolean isEthiopic(int c) {
+               if ((c >= 0x1200) && (c <= 0x137F)) {             // ethiopic block
+                       return true;
+               } else if ((c >= 0x1380) && (c <= 0x139F)) {      // ethoipic supplement block
+                       return true;
+               } else if ((c >= 0x2D80) && (c <= 0x2DDF)) {      // ethoipic extended block
+                       return true;
+               } else // ethoipic extended-a block
+                       return (c >= 0xAB00) && (c <= 0xAB2F);
+       }
+
+       /**
+        * Determine if character c belong to the han (unified cjk) script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to han (unified cjk) script
+        */
+       public static boolean isHan(int c) {
+               if ((c >= 0x3400) && (c <= 0x4DBF)) {
+                       return true; // cjk unified ideographs extension a
+               } else if ((c >= 0x4E00) && (c <= 0x9FFF)) {
+                       return true; // cjk unified ideographs
+               } else if ((c >= 0xF900) && (c <= 0xFAFF)) {
+                       return true; // cjk compatibility ideographs
+               } else if ((c >= 0x20000) && (c <= 0x2A6DF)) {
+                       return true; // cjk unified ideographs extension b
+               } else if ((c >= 0x2A700) && (c <= 0x2B73F)) {
+                       return true; // cjk unified ideographs extension c
+               } else // cjk compatibility ideographs supplement
+                       return (c >= 0x2F800) && (c <= 0x2FA1F);
+       }
+
+       /**
+        * Determine if character c belong to the bopomofo script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to bopomofo script
+        */
+       public static boolean isBopomofo(int c) {
+               return (c >= 0x3100) && (c <= 0x312F);
+       }
+
+       /**
+        * Determine if character c belong to the hiragana script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to hiragana script
+        */
+       public static boolean isHiragana(int c) {
+               return (c >= 0x3040) && (c <= 0x309F);
+       }
+
+       /**
+        * Determine if character c belong to the katakana script.
+        *
+        * @param c a character represented as a unicode scalar value
+        * @return true if character belongs to katakana script
+        */
+       public static boolean isKatakana(int c) {
+               if ((c >= 0x30A0) && (c <= 0x30FF)) {
+                       return true;
+               } else return (c >= 0x31F0) && (c <= 0x31FF);
+       }
+
+       /**
+        * Obtain ISO15924 numeric script code of character. If script is not or cannot be determined,
+        * then the script code 998 ('zyyy') is returned.
+        *
+        * @param c the character to obtain script
+        * @return an ISO15924 script code
+        */
+       public static int scriptOf(int c) { // [TBD] - needs optimization!!!
+               if (CharUtilities.isAnySpace(c)) {
+                       return SCRIPT_UNDETERMINED;
+               } else if (isPunctuation(c)) {
+                       return SCRIPT_UNDETERMINED;
+               } else if (isDigit(c)) {
+                       return SCRIPT_UNDETERMINED;
+               } else if (isLatin(c)) {
+                       return SCRIPT_LATIN;
+               } else if (isCyrillic(c)) {
+                       return SCRIPT_CYRILLIC;
+               } else if (isGreek(c)) {
+                       return SCRIPT_GREEK;
+               } else if (isHan(c)) {
+                       return SCRIPT_HAN;
+               } else if (isBopomofo(c)) {
+                       return SCRIPT_BOPOMOFO;
+               } else if (isKatakana(c)) {
+                       return SCRIPT_KATAKANA;
+               } else if (isHiragana(c)) {
+                       return SCRIPT_HIRAGANA;
+               } else if (isHangul(c)) {
+                       return SCRIPT_HANGUL;
+               } else if (isArabic(c)) {
+                       return SCRIPT_ARABIC;
+               } else if (isHebrew(c)) {
+                       return SCRIPT_HEBREW;
+               } else if (isMongolian(c)) {
+                       return SCRIPT_MONGOLIAN;
+               } else if (isGeorgian(c)) {
+                       return SCRIPT_GEORGIAN;
+               } else if (isGurmukhi(c)) {
+                       return useV2IndicRules(SCRIPT_GURMUKHI);
+               } else if (isDevanagari(c)) {
+                       return useV2IndicRules(SCRIPT_DEVANAGARI);
+               } else if (isGujarati(c)) {
+                       return useV2IndicRules(SCRIPT_GUJARATI);
+               } else if (isBengali(c)) {
+                       return useV2IndicRules(SCRIPT_BENGALI);
+               } else if (isOriya(c)) {
+                       return useV2IndicRules(SCRIPT_ORIYA);
+               } else if (isTibetan(c)) {
+                       return SCRIPT_TIBETAN;
+               } else if (isTelugu(c)) {
+                       return useV2IndicRules(SCRIPT_TELUGU);
+               } else if (isKannada(c)) {
+                       return useV2IndicRules(SCRIPT_KANNADA);
+               } else if (isTamil(c)) {
+                       return useV2IndicRules(SCRIPT_TAMIL);
+               } else if (isMalayalam(c)) {
+                       return useV2IndicRules(SCRIPT_MALAYALAM);
+               } else if (isSinhalese(c)) {
+                       return SCRIPT_SINHALESE;
+               } else if (isBurmese(c)) {
+                       return SCRIPT_BURMESE;
+               } else if (isThai(c)) {
+                       return SCRIPT_THAI;
+               } else if (isKhmer(c)) {
+                       return SCRIPT_KHMER;
+               } else if (isLao(c)) {
+                       return SCRIPT_LAO;
+               } else if (isEthiopic(c)) {
+                       return SCRIPT_ETHIOPIC;
+               } else {
+                       return SCRIPT_UNDETERMINED;
+               }
+       }
+
+       /**
+        * Obtain the V2 indic script code corresponding to V1 indic script code SC if
+        * and only iff V2 indic rules apply; otherwise return SC.
+        *
+        * @param sc a V1 indic script code
+        * @return either SC or the V2 flavor of SC if V2 indic rules apply
+        */
+       public static int useV2IndicRules(int sc) {
+               if (USE_V2_INDIC) {
+                       return (sc < 1000) ? (sc + 1000) : sc;
+               } else {
+                       return sc;
+               }
+       }
+
+       /**
+        * Obtain the  script codes of each character in a character sequence. If script
+        * is not or cannot be determined for some character, then the script code 998
+        * ('zyyy') is returned.
+        *
+        * @param cs the character sequence
+        * @return a (possibly empty) array of script codes
+        */
+       public static int[] scriptsOf(CharSequence cs) {
+               Set s = new HashSet();
+               for (int i = 0, n = cs.length(); i < n; i++) {
+                       s.add(Integer.valueOf(scriptOf(cs.charAt(i))));
+               }
+               int[] sa = new int[s.size()];
+               int ns = 0;
+               for (Iterator it = s.iterator(); it.hasNext(); ) {
+                       sa[ns++] = ((Integer) it.next()).intValue();
+               }
+               Arrays.sort(sa);
+               return sa;
+       }
+
+       /**
+        * Determine the dominant script of a character sequence.
+        *
+        * @param cs the character sequence
+        * @return the dominant script or SCRIPT_UNDETERMINED
+        */
+       public static int dominantScript(CharSequence cs) {
+               Map m = new HashMap();
+               for (int i = 0, n = cs.length(); i < n; i++) {
+                       int c = cs.charAt(i);
+                       int s = scriptOf(c);
+                       Integer k = Integer.valueOf(s);
+                       Integer v = (Integer) m.get(k);
+                       if (v != null) {
+                               m.put(k, Integer.valueOf(v.intValue() + 1));
+                       } else {
+                               m.put(k, Integer.valueOf(0));
+                       }
+               }
+               int sMax = -1;
+               int cMax = -1;
+               for (Iterator it = m.entrySet().iterator(); it.hasNext(); ) {
+                       Map.Entry e = (Map.Entry) it.next();
+                       Integer k = (Integer) e.getKey();
+                       int s = k.intValue();
+                       switch (s) {
+                               case SCRIPT_UNDETERMINED:
+                               case SCRIPT_UNCODED:
+                                       break;
+                               default:
+                                       Integer v = (Integer) e.getValue();
+                                       assert v != null;
+                                       int c = v.intValue();
+                                       if (c > cMax) {
+                                               cMax = c;
+                                               sMax = s;
+                                       }
+                                       break;
+                       }
+               }
+               if (sMax < 0) {
+                       sMax = SCRIPT_UNDETERMINED;
+               }
+               return sMax;
+       }
+
+       /**
+        * Determine if script tag denotes an 'Indic' script, where a
+        * script is an 'Indic' script if it is intended to be processed by
+        * the generic 'Indic' Script Processor.
+        *
+        * @param script a script tag
+        * @return true if script tag is a designated 'Indic' script
+        */
+       public static boolean isIndicScript(String script) {
+               return isIndicScript(scriptCodeFromTag(script));
+       }
+
+       /**
+        * Determine if script tag denotes an 'Indic' script, where a
+        * script is an 'Indic' script if it is intended to be processed by
+        * the generic 'Indic' Script Processor.
+        *
+        * @param script a script code
+        * @return true if script code is a designated 'Indic' script
+        */
+       public static boolean isIndicScript(int script) {
+               switch (script) {
+                       case SCRIPT_BENGALI:
+                       case SCRIPT_BENGALI_2:
+                       case SCRIPT_BURMESE:
+                       case SCRIPT_DEVANAGARI:
+                       case SCRIPT_DEVANAGARI_2:
+                       case SCRIPT_GUJARATI:
+                       case SCRIPT_GUJARATI_2:
+                       case SCRIPT_GURMUKHI:
+                       case SCRIPT_GURMUKHI_2:
+                       case SCRIPT_KANNADA:
+                       case SCRIPT_KANNADA_2:
+                       case SCRIPT_MALAYALAM:
+                       case SCRIPT_MALAYALAM_2:
+                       case SCRIPT_ORIYA:
+                       case SCRIPT_ORIYA_2:
+                       case SCRIPT_TAMIL:
+                       case SCRIPT_TAMIL_2:
+                       case SCRIPT_TELUGU:
+                       case SCRIPT_TELUGU_2:
+                               return true;
+                       default:
+                               return false;
+               }
+       }
+
+       /**
+        * Determine the script tag associated with an internal script code.
+        *
+        * @param code the script code
+        * @return a  script tag
+        */
+       public static String scriptTagFromCode(int code) {
+               Map<Integer, String> m = getScriptTagsMap();
+               if (m != null) {
+                       String tag;
+                       if ((tag = m.get(Integer.valueOf(code))) != null) {
+                               return tag;
+                       } else {
+                               return "";
+                       }
+               } else {
+                       return "";
+               }
+       }
+
+       /**
+        * Determine the internal script code associated with a script tag.
+        *
+        * @param tag the script tag
+        * @return a script code
+        */
+       public static int scriptCodeFromTag(String tag) {
+               Map<String, Integer> m = getScriptCodeMap();
+               if (m != null) {
+                       Integer c;
+                       if ((c = m.get(tag)) != null) {
+                               return c;
+                       } else {
+                               return SCRIPT_UNDETERMINED;
+                       }
+               } else {
+                       return SCRIPT_UNDETERMINED;
+               }
+       }
+
+       private static Map<Integer, String> scriptTagsMap;
+       private static Map<String, Integer> scriptCodeMap;
+
+       private static void putScriptTag(Map tm, Map cm, int code, String tag) {
+               assert tag != null;
+               assert tag.length() != 0;
+               assert code >= 0;
+               assert code < 2000;
+               tm.put(Integer.valueOf(code), tag);
+               cm.put(tag, Integer.valueOf(code));
+       }
+
+       private static void makeScriptMaps() {
+               HashMap<Integer, String> tm = new HashMap<Integer, String>();
+               HashMap<String, Integer> cm = new HashMap<String, Integer>();
+               putScriptTag(tm, cm, SCRIPT_HEBREW, "hebr");
+               putScriptTag(tm, cm, SCRIPT_MONGOLIAN, "mong");
+               putScriptTag(tm, cm, SCRIPT_ARABIC, "arab");
+               putScriptTag(tm, cm, SCRIPT_GREEK, "grek");
+               putScriptTag(tm, cm, SCRIPT_LATIN, "latn");
+               putScriptTag(tm, cm, SCRIPT_CYRILLIC, "cyrl");
+               putScriptTag(tm, cm, SCRIPT_GEORGIAN, "geor");
+               putScriptTag(tm, cm, SCRIPT_BOPOMOFO, "bopo");
+               putScriptTag(tm, cm, SCRIPT_HANGUL, "hang");
+               putScriptTag(tm, cm, SCRIPT_GURMUKHI, "guru");
+               putScriptTag(tm, cm, SCRIPT_GURMUKHI_2, "gur2");
+               putScriptTag(tm, cm, SCRIPT_DEVANAGARI, "deva");
+               putScriptTag(tm, cm, SCRIPT_DEVANAGARI_2, "dev2");
+               putScriptTag(tm, cm, SCRIPT_GUJARATI, "gujr");
+               putScriptTag(tm, cm, SCRIPT_GUJARATI_2, "gjr2");
+               putScriptTag(tm, cm, SCRIPT_BENGALI, "beng");
+               putScriptTag(tm, cm, SCRIPT_BENGALI_2, "bng2");
+               putScriptTag(tm, cm, SCRIPT_ORIYA, "orya");
+               putScriptTag(tm, cm, SCRIPT_ORIYA_2, "ory2");
+               putScriptTag(tm, cm, SCRIPT_TIBETAN, "tibt");
+               putScriptTag(tm, cm, SCRIPT_TELUGU, "telu");
+               putScriptTag(tm, cm, SCRIPT_TELUGU_2, "tel2");
+               putScriptTag(tm, cm, SCRIPT_KANNADA, "knda");
+               putScriptTag(tm, cm, SCRIPT_KANNADA_2, "knd2");
+               putScriptTag(tm, cm, SCRIPT_TAMIL, "taml");
+               putScriptTag(tm, cm, SCRIPT_TAMIL_2, "tml2");
+               putScriptTag(tm, cm, SCRIPT_MALAYALAM, "mlym");
+               putScriptTag(tm, cm, SCRIPT_MALAYALAM_2, "mlm2");
+               putScriptTag(tm, cm, SCRIPT_SINHALESE, "sinh");
+               putScriptTag(tm, cm, SCRIPT_BURMESE, "mymr");
+               putScriptTag(tm, cm, SCRIPT_THAI, "thai");
+               putScriptTag(tm, cm, SCRIPT_KHMER, "khmr");
+               putScriptTag(tm, cm, SCRIPT_LAO, "laoo");
+               putScriptTag(tm, cm, SCRIPT_HIRAGANA, "hira");
+               putScriptTag(tm, cm, SCRIPT_ETHIOPIC, "ethi");
+               putScriptTag(tm, cm, SCRIPT_HAN, "hani");
+               putScriptTag(tm, cm, SCRIPT_KATAKANA, "kana");
+               putScriptTag(tm, cm, SCRIPT_MATH, "zmth");
+               putScriptTag(tm, cm, SCRIPT_SYMBOL, "zsym");
+               putScriptTag(tm, cm, SCRIPT_UNDETERMINED, "zyyy");
+               putScriptTag(tm, cm, SCRIPT_UNCODED, "zzzz");
+               scriptTagsMap = tm;
+               scriptCodeMap = cm;
+       }
+
+       private static Map<Integer, String> getScriptTagsMap() {
+               if (scriptTagsMap == null) {
+                       makeScriptMaps();
+               }
+               return scriptTagsMap;
+       }
+
+       private static Map<String, Integer> getScriptCodeMap() {
+               if (scriptCodeMap == null) {
+                       makeScriptMaps();
+               }
+               return scriptCodeMap;
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CMapSegment.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CMapSegment.java
new file mode 100644 (file)
index 0000000..177f56a
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+/**
+ * A segment in a cmap table of format 4. Unicode code points between
+ * {@link #getUnicodeStart()} and {@link #getUnicodeEnd()} map to contiguous glyph indices
+ * starting from {@link #getGlyphStartIndex()}.
+ */
+public final class CMapSegment {
+
+       private final int unicodeStart;
+       private final int unicodeEnd;
+       private final int glyphStartIndex;
+
+       /**
+        * Creates a new segment.
+        *
+        * @param unicodeStart    Unicode start index
+        * @param unicodeEnd      Unicode end index
+        * @param glyphStartIndex glyph start index
+        */
+       public CMapSegment(int unicodeStart, int unicodeEnd, int glyphStartIndex) {
+               this.unicodeStart = unicodeStart;
+               this.unicodeEnd = unicodeEnd;
+               this.glyphStartIndex = glyphStartIndex;
+       }
+
+       @Override
+       public int hashCode() {
+               int hc = 17;
+               hc = 31 * hc + unicodeStart;
+               hc = 31 * hc + unicodeEnd;
+               hc = 31 * hc + glyphStartIndex;
+               return hc;
+       }
+
+       @Override
+       public boolean equals(Object o) {
+               if (o instanceof CMapSegment) {
+                       CMapSegment ce = (CMapSegment) o;
+                       return ce.unicodeStart == this.unicodeStart
+                                       && ce.unicodeEnd == this.unicodeEnd
+                                       && ce.glyphStartIndex == this.glyphStartIndex;
+               }
+               return false;
+       }
+
+       /**
+        * Returns the unicodeStart.
+        *
+        * @return the Unicode start index
+        */
+       public int getUnicodeStart() {
+               return unicodeStart;
+       }
+
+       /**
+        * Returns the unicodeEnd.
+        *
+        * @return the Unicode end index
+        */
+       public int getUnicodeEnd() {
+               return unicodeEnd;
+       }
+
+       /**
+        * Returns the glyphStartIndex.
+        *
+        * @return the glyph start index
+        */
+       public int getGlyphStartIndex() {
+               return glyphStartIndex;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               StringBuilder sb = new StringBuilder("CMapSegment: ");
+               sb.append("{ UC[");
+               sb.append(unicodeStart);
+               sb.append(',');
+               sb.append(unicodeEnd);
+               sb.append("]: GC[");
+               sb.append(glyphStartIndex);
+               sb.append(',');
+               sb.append(glyphStartIndex + (unicodeEnd - unicodeStart));
+               sb.append("] }");
+               return sb.toString();
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CodePointMapping.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CodePointMapping.java
new file mode 100644 (file)
index 0000000..40e20f1
--- /dev/null
@@ -0,0 +1,1804 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+import java.util.Collections;
+import java.util.Map;
+
+public class CodePointMapping {
+
+       private char[] latin1Map;
+       private char[] characters;
+       private char[] codepoints;
+
+       private CodePointMapping(int[] table) {
+               int nonLatin1 = 0;
+               latin1Map = new char[256];
+               for (int i = 0; i < table.length; i += 2) {
+                       if (table[i + 1] < 256)
+                               latin1Map[table[i + 1]] = (char) table[i];
+                       else
+                               ++nonLatin1;
+               }
+               characters = new char[nonLatin1];
+               codepoints = new char[nonLatin1];
+               int top = 0;
+               for (int i = 0; i < table.length; i += 2) {
+                       char c = (char) table[i + 1];
+                       if (c >= 256) {
+                               ++top;
+                               for (int j = top - 1; j >= 0; --j) {
+                                       if (j > 0 && characters[j - 1] >= c) {
+                                               characters[j] = characters[j - 1];
+                                               codepoints[j] = codepoints[j - 1];
+                                       } else {
+                                               characters[j] = c;
+                                               codepoints[j] = (char) table[i];
+                                               break;
+                                       }
+                               }
+                       }
+               }
+       }
+
+       public final char mapChar(char c) {
+               if (c < 256) {
+                       return latin1Map[c];
+               } else {
+                       int bot = 0, top = characters.length - 1;
+                       while (top >= bot) {
+                               int mid = (bot + top) / 2;
+                               char mc = characters[mid];
+
+                               if (c == mc)
+                                       return codepoints[mid];
+                               else if (c < mc)
+                                       top = mid - 1;
+                               else
+                                       bot = mid + 1;
+                       }
+                       return 0;
+               }
+       }
+
+       private static Map mappings;
+
+       static {
+               mappings = Collections.synchronizedMap(new java.util.HashMap());
+       }
+
+       public static CodePointMapping getMapping(String encoding) {
+               CodePointMapping mapping = (CodePointMapping) mappings.get(encoding);
+               if (mapping != null) {
+                       return mapping;
+               } else if (encoding.equals("StandardEncoding")) {
+                       mapping = new CodePointMapping(encStandardEncoding);
+                       mappings.put("StandardEncoding", mapping);
+                       return mapping;
+               } else if (encoding.equals("ISOLatin1Encoding")) {
+                       mapping = new CodePointMapping(encISOLatin1Encoding);
+                       mappings.put("ISOLatin1Encoding", mapping);
+                       return mapping;
+               } else if (encoding.equals("CEEncoding")) {
+                       mapping = new CodePointMapping(encCEEncoding);
+                       mappings.put("CEEncoding", mapping);
+                       return mapping;
+               } else if (encoding.equals("MacRomanEncoding")) {
+                       mapping = new CodePointMapping(encMacRomanEncoding);
+                       mappings.put("MacRomanEncoding", mapping);
+                       return mapping;
+               } else if (encoding.equals("WinAnsiEncoding")) {
+                       mapping = new CodePointMapping(encWinAnsiEncoding);
+                       mappings.put("WinAnsiEncoding", mapping);
+                       return mapping;
+               } else if (encoding.equals("PDFDocEncoding")) {
+                       mapping = new CodePointMapping(encPDFDocEncoding);
+                       mappings.put("PDFDocEncoding", mapping);
+                       return mapping;
+               } else if (encoding.equals("SymbolEncoding")) {
+                       mapping = new CodePointMapping(encSymbolEncoding);
+                       mappings.put("SymbolEncoding", mapping);
+                       return mapping;
+               } else if (encoding.equals("ZapfDingbatsEncoding")) {
+                       mapping = new CodePointMapping(encZapfDingbatsEncoding);
+                       mappings.put("ZapfDingbatsEncoding", mapping);
+                       return mapping;
+               } else {
+                       return null;
+               }
+       }
+
+       private static final int[] encStandardEncoding
+                       = {
+                       0x20, 0x0020, // space
+                       0x20, 0x00A0, // space
+                       0x21, 0x0021, // exclam
+                       0x22, 0x0022, // quotedbl
+                       0x23, 0x0023, // numbersign
+                       0x24, 0x0024, // dollar
+                       0x25, 0x0025, // percent
+                       0x26, 0x0026, // ampersand
+                       0x27, 0x2019, // quoteright
+                       0x28, 0x0028, // parenleft
+                       0x29, 0x0029, // parenright
+                       0x2a, 0x002A, // asterisk
+                       0x2b, 0x002B, // plus
+                       0x2c, 0x002C, // comma
+                       0x2d, 0x002D, // hyphen
+                       0x2d, 0x00AD, // hyphen
+                       0x2e, 0x002E, // period
+                       0x2f, 0x002F, // slash
+                       0x30, 0x0030, // zero
+                       0x31, 0x0031, // one
+                       0x32, 0x0032, // two
+                       0x33, 0x0033, // three
+                       0x34, 0x0034, // four
+                       0x35, 0x0035, // five
+                       0x36, 0x0036, // six
+                       0x37, 0x0037, // seven
+                       0x38, 0x0038, // eight
+                       0x39, 0x0039, // nine
+                       0x3a, 0x003A, // colon
+                       0x3b, 0x003B, // semicolon
+                       0x3c, 0x003C, // less
+                       0x3d, 0x003D, // equal
+                       0x3e, 0x003E, // greater
+                       0x3f, 0x003F, // question
+                       0x40, 0x0040, // at
+                       0x41, 0x0041, // A
+                       0x42, 0x0042, // B
+                       0x43, 0x0043, // C
+                       0x44, 0x0044, // D
+                       0x45, 0x0045, // E
+                       0x46, 0x0046, // F
+                       0x47, 0x0047, // G
+                       0x48, 0x0048, // H
+                       0x49, 0x0049, // I
+                       0x4a, 0x004A, // J
+                       0x4b, 0x004B, // K
+                       0x4c, 0x004C, // L
+                       0x4d, 0x004D, // M
+                       0x4e, 0x004E, // N
+                       0x4f, 0x004F, // O
+                       0x50, 0x0050, // P
+                       0x51, 0x0051, // Q
+                       0x52, 0x0052, // R
+                       0x53, 0x0053, // S
+                       0x54, 0x0054, // T
+                       0x55, 0x0055, // U
+                       0x56, 0x0056, // V
+                       0x57, 0x0057, // W
+                       0x58, 0x0058, // X
+                       0x59, 0x0059, // Y
+                       0x5a, 0x005A, // Z
+                       0x5b, 0x005B, // bracketleft
+                       0x5c, 0x005C, // backslash
+                       0x5d, 0x005D, // bracketright
+                       0x5e, 0x005E, // asciicircum
+                       0x5f, 0x005F, // underscore
+                       0x60, 0x2018, // quoteleft
+                       0x61, 0x0061, // a
+                       0x62, 0x0062, // b
+                       0x63, 0x0063, // c
+                       0x64, 0x0064, // d
+                       0x65, 0x0065, // e
+                       0x66, 0x0066, // f
+                       0x67, 0x0067, // g
+                       0x68, 0x0068, // h
+                       0x69, 0x0069, // i
+                       0x6a, 0x006A, // j
+                       0x6b, 0x006B, // k
+                       0x6c, 0x006C, // l
+                       0x6d, 0x006D, // m
+                       0x6e, 0x006E, // n
+                       0x6f, 0x006F, // o
+                       0x70, 0x0070, // p
+                       0x71, 0x0071, // q
+                       0x72, 0x0072, // r
+                       0x73, 0x0073, // s
+                       0x74, 0x0074, // t
+                       0x75, 0x0075, // u
+                       0x76, 0x0076, // v
+                       0x77, 0x0077, // w
+                       0x78, 0x0078, // x
+                       0x79, 0x0079, // y
+                       0x7a, 0x007A, // z
+                       0x7b, 0x007B, // braceleft
+                       0x7c, 0x007C, // bar
+                       0x7d, 0x007D, // braceright
+                       0x7e, 0x007E, // asciitilde
+                       0xa1, 0x00A1, // exclamdown
+                       0xa2, 0x00A2, // cent
+                       0xa3, 0x00A3, // sterling
+                       0xa4, 0x2044, // fraction
+                       0xa4, 0x2215, // fraction
+                       0xa5, 0x00A5, // yen
+                       0xa6, 0x0192, // florin
+                       0xa7, 0x00A7, // section
+                       0xa8, 0x00A4, // currency
+                       0xa9, 0x0027, // quotesingle
+                       0xaa, 0x201C, // quotedblleft
+                       0xab, 0x00AB, // guillemotleft
+                       0xac, 0x2039, // guilsinglleft
+                       0xad, 0x203A, // guilsinglright
+                       0xae, 0xFB01, // fi
+                       0xaf, 0xFB02, // fl
+                       0xb1, 0x2013, // endash
+                       0xb2, 0x2020, // dagger
+                       0xb3, 0x2021, // daggerdbl
+                       0xb4, 0x00B7, // periodcentered
+                       0xb4, 0x2219, // periodcentered
+                       0xb6, 0x00B6, // paragraph
+                       0xb7, 0x2022, // bullet
+                       0xb8, 0x201A, // quotesinglbase
+                       0xb9, 0x201E, // quotedblbase
+                       0xba, 0x201D, // quotedblright
+                       0xbb, 0x00BB, // guillemotright
+                       0xbc, 0x2026, // ellipsis
+                       0xbd, 0x2030, // perthousand
+                       0xbf, 0x00BF, // questiondown
+                       0xc1, 0x0060, // grave
+                       0xc2, 0x00B4, // acute
+                       0xc3, 0x02C6, // circumflex
+                       0xc4, 0x02DC, // tilde
+                       0xc5, 0x00AF, // macron
+                       0xc5, 0x02C9, // macron
+                       0xc6, 0x02D8, // breve
+                       0xc7, 0x02D9, // dotaccent
+                       0xc8, 0x00A8, // dieresis
+                       0xca, 0x02DA, // ring
+                       0xcb, 0x00B8, // cedilla
+                       0xcd, 0x02DD, // hungarumlaut
+                       0xce, 0x02DB, // ogonek
+                       0xcf, 0x02C7, // caron
+                       0xd0, 0x2014, // emdash
+                       0xe1, 0x00C6, // AE
+                       0xe3, 0x00AA, // ordfeminine
+                       0xe8, 0x0141, // Lslash
+                       0xe9, 0x00D8, // Oslash
+                       0xea, 0x0152, // OE
+                       0xeb, 0x00BA, // ordmasculine
+                       0xf1, 0x00E6, // ae
+                       0xf5, 0x0131, // dotlessi
+                       0xf8, 0x0142, // lslash
+                       0xf9, 0x00F8, // oslash
+                       0xfa, 0x0153, // oe
+                       0xfb, 0x00DF, // germandbls
+       };
+
+       private static final int[] encISOLatin1Encoding
+                       = {
+                       0x20, 0x0020, // space
+                       0x20, 0x00A0, // space
+                       0x21, 0x0021, // exclam
+                       0x22, 0x0022, // quotedbl
+                       0x23, 0x0023, // numbersign
+                       0x24, 0x0024, // dollar
+                       0x25, 0x0025, // percent
+                       0x26, 0x0026, // ampersand
+                       0x27, 0x2019, // quoteright
+                       0x28, 0x0028, // parenleft
+                       0x29, 0x0029, // parenright
+                       0x2a, 0x002A, // asterisk
+                       0x2b, 0x002B, // plus
+                       0x2c, 0x002C, // comma
+                       0x2d, 0x2212, // minus
+                       0x2e, 0x002E, // period
+                       0x2f, 0x002F, // slash
+                       0x30, 0x0030, // zero
+                       0x31, 0x0031, // one
+                       0x32, 0x0032, // two
+                       0x33, 0x0033, // three
+                       0x34, 0x0034, // four
+                       0x35, 0x0035, // five
+                       0x36, 0x0036, // six
+                       0x37, 0x0037, // seven
+                       0x38, 0x0038, // eight
+                       0x39, 0x0039, // nine
+                       0x3a, 0x003A, // colon
+                       0x3b, 0x003B, // semicolon
+                       0x3c, 0x003C, // less
+                       0x3d, 0x003D, // equal
+                       0x3e, 0x003E, // greater
+                       0x3f, 0x003F, // question
+                       0x40, 0x0040, // at
+                       0x41, 0x0041, // A
+                       0x42, 0x0042, // B
+                       0x43, 0x0043, // C
+                       0x44, 0x0044, // D
+                       0x45, 0x0045, // E
+                       0x46, 0x0046, // F
+                       0x47, 0x0047, // G
+                       0x48, 0x0048, // H
+                       0x49, 0x0049, // I
+                       0x4a, 0x004A, // J
+                       0x4b, 0x004B, // K
+                       0x4c, 0x004C, // L
+                       0x4d, 0x004D, // M
+                       0x4e, 0x004E, // N
+                       0x4f, 0x004F, // O
+                       0x50, 0x0050, // P
+                       0x51, 0x0051, // Q
+                       0x52, 0x0052, // R
+                       0x53, 0x0053, // S
+                       0x54, 0x0054, // T
+                       0x55, 0x0055, // U
+                       0x56, 0x0056, // V
+                       0x57, 0x0057, // W
+                       0x58, 0x0058, // X
+                       0x59, 0x0059, // Y
+                       0x5a, 0x005A, // Z
+                       0x5b, 0x005B, // bracketleft
+                       0x5c, 0x005C, // backslash
+                       0x5d, 0x005D, // bracketright
+                       0x5e, 0x005E, // asciicircum
+                       0x5f, 0x005F, // underscore
+                       0x60, 0x2018, // quoteleft
+                       0x61, 0x0061, // a
+                       0x62, 0x0062, // b
+                       0x63, 0x0063, // c
+                       0x64, 0x0064, // d
+                       0x65, 0x0065, // e
+                       0x66, 0x0066, // f
+                       0x67, 0x0067, // g
+                       0x68, 0x0068, // h
+                       0x69, 0x0069, // i
+                       0x6a, 0x006A, // j
+                       0x6b, 0x006B, // k
+                       0x6c, 0x006C, // l
+                       0x6d, 0x006D, // m
+                       0x6e, 0x006E, // n
+                       0x6f, 0x006F, // o
+                       0x70, 0x0070, // p
+                       0x71, 0x0071, // q
+                       0x72, 0x0072, // r
+                       0x73, 0x0073, // s
+                       0x74, 0x0074, // t
+                       0x75, 0x0075, // u
+                       0x76, 0x0076, // v
+                       0x77, 0x0077, // w
+                       0x78, 0x0078, // x
+                       0x79, 0x0079, // y
+                       0x7a, 0x007A, // z
+                       0x7b, 0x007B, // braceleft
+                       0x7c, 0x007C, // bar
+                       0x7d, 0x007D, // braceright
+                       0x7e, 0x007E, // asciitilde
+                       0x90, 0x0131, // dotlessi
+                       0x91, 0x0060, // grave
+                       0x93, 0x02C6, // circumflex
+                       0x94, 0x02DC, // tilde
+                       0x96, 0x02D8, // breve
+                       0x97, 0x02D9, // dotaccent
+                       0x9a, 0x02DA, // ring
+                       0x9d, 0x02DD, // hungarumlaut
+                       0x9e, 0x02DB, // ogonek
+                       0x9f, 0x02C7, // caron
+                       0xa1, 0x00A1, // exclamdown
+                       0xa2, 0x00A2, // cent
+                       0xa3, 0x00A3, // sterling
+                       0xa4, 0x00A4, // currency
+                       0xa5, 0x00A5, // yen
+                       0xa6, 0x00A6, // brokenbar
+                       0xa7, 0x00A7, // section
+                       0xa8, 0x00A8, // dieresis
+                       0xa9, 0x00A9, // copyright
+                       0xaa, 0x00AA, // ordfeminine
+                       0xab, 0x00AB, // guillemotleft
+                       0xac, 0x00AC, // logicalnot
+                       0xad, 0x002D, // hyphen
+                       0xad, 0x00AD, // hyphen
+                       0xae, 0x00AE, // registered
+                       0xaf, 0x00AF, // macron
+                       0xaf, 0x02C9, // macron
+                       0xb0, 0x00B0, // degree
+                       0xb1, 0x00B1, // plusminus
+                       0xb2, 0x00B2, // twosuperior
+                       0xb3, 0x00B3, // threesuperior
+                       0xb4, 0x00B4, // acute
+                       0xb5, 0x00B5, // mu
+                       0xb5, 0x03BC, // mu
+                       0xb6, 0x00B6, // paragraph
+                       0xb7, 0x00B7, // periodcentered
+                       0xb7, 0x2219, // periodcentered
+                       0xb8, 0x00B8, // cedilla
+                       0xb9, 0x00B9, // onesuperior
+                       0xba, 0x00BA, // ordmasculine
+                       0xbb, 0x00BB, // guillemotright
+                       0xbc, 0x00BC, // onequarter
+                       0xbd, 0x00BD, // onehalf
+                       0xbe, 0x00BE, // threequarters
+                       0xbf, 0x00BF, // questiondown
+                       0xc0, 0x00C0, // Agrave
+                       0xc1, 0x00C1, // Aacute
+                       0xc2, 0x00C2, // Acircumflex
+                       0xc3, 0x00C3, // Atilde
+                       0xc4, 0x00C4, // Adieresis
+                       0xc5, 0x00C5, // Aring
+                       0xc6, 0x00C6, // AE
+                       0xc7, 0x00C7, // Ccedilla
+                       0xc8, 0x00C8, // Egrave
+                       0xc9, 0x00C9, // Eacute
+                       0xca, 0x00CA, // Ecircumflex
+                       0xcb, 0x00CB, // Edieresis
+                       0xcc, 0x00CC, // Igrave
+                       0xcd, 0x00CD, // Iacute
+                       0xce, 0x00CE, // Icircumflex
+                       0xcf, 0x00CF, // Idieresis
+                       0xd0, 0x00D0, // Eth
+                       0xd1, 0x00D1, // Ntilde
+                       0xd2, 0x00D2, // Ograve
+                       0xd3, 0x00D3, // Oacute
+                       0xd4, 0x00D4, // Ocircumflex
+                       0xd5, 0x00D5, // Otilde
+                       0xd6, 0x00D6, // Odieresis
+                       0xd7, 0x00D7, // multiply
+                       0xd8, 0x00D8, // Oslash
+                       0xd9, 0x00D9, // Ugrave
+                       0xda, 0x00DA, // Uacute
+                       0xdb, 0x00DB, // Ucircumflex
+                       0xdc, 0x00DC, // Udieresis
+                       0xdd, 0x00DD, // Yacute
+                       0xde, 0x00DE, // Thorn
+                       0xdf, 0x00DF, // germandbls
+                       0xe0, 0x00E0, // agrave
+                       0xe1, 0x00E1, // aacute
+                       0xe2, 0x00E2, // acircumflex
+                       0xe3, 0x00E3, // atilde
+                       0xe4, 0x00E4, // adieresis
+                       0xe5, 0x00E5, // aring
+                       0xe6, 0x00E6, // ae
+                       0xe7, 0x00E7, // ccedilla
+                       0xe8, 0x00E8, // egrave
+                       0xe9, 0x00E9, // eacute
+                       0xea, 0x00EA, // ecircumflex
+                       0xeb, 0x00EB, // edieresis
+                       0xec, 0x00EC, // igrave
+                       0xed, 0x00ED, // iacute
+                       0xee, 0x00EE, // icircumflex
+                       0xef, 0x00EF, // idieresis
+                       0xf0, 0x00F0, // eth
+                       0xf1, 0x00F1, // ntilde
+                       0xf2, 0x00F2, // ograve
+                       0xf3, 0x00F3, // oacute
+                       0xf4, 0x00F4, // ocircumflex
+                       0xf5, 0x00F5, // otilde
+                       0xf6, 0x00F6, // odieresis
+                       0xf7, 0x00F7, // divide
+                       0xf8, 0x00F8, // oslash
+                       0xf9, 0x00F9, // ugrave
+                       0xfa, 0x00FA, // uacute
+                       0xfb, 0x00FB, // ucircumflex
+                       0xfc, 0x00FC, // udieresis
+                       0xfd, 0x00FD, // yacute
+                       0xfe, 0x00FE, // thorn
+                       0xff, 0x00FF, // ydieresis
+       };
+
+       private static final int[] encCEEncoding
+                       = {
+                       0x20, 0x0020, // space
+                       0x20, 0x00A0, // space
+                       0x21, 0x0021, // exclam
+                       0x22, 0x0022, // quotedbl
+                       0x23, 0x0023, // numbersign
+                       0x24, 0x0024, // dollar
+                       0x25, 0x0025, // percent
+                       0x26, 0x0026, // ampersand
+                       0x27, 0x0027, // quotesingle
+                       0x28, 0x0028, // parenleft
+                       0x29, 0x0029, // parenright
+                       0x2a, 0x002A, // asterisk
+                       0x2b, 0x002B, // plus
+                       0x2c, 0x002C, // comma
+                       0x2d, 0x002D, // hyphen
+                       0x2d, 0x00AD, // hyphen
+                       0x2e, 0x002E, // period
+                       0x2f, 0x002F, // slash
+                       0x30, 0x0030, // zero
+                       0x31, 0x0031, // one
+                       0x32, 0x0032, // two
+                       0x33, 0x0033, // three
+                       0x34, 0x0034, // four
+                       0x35, 0x0035, // five
+                       0x36, 0x0036, // six
+                       0x37, 0x0037, // seven
+                       0x38, 0x0038, // eight
+                       0x39, 0x0039, // nine
+                       0x3a, 0x003A, // colon
+                       0x3b, 0x003B, // semicolon
+                       0x3c, 0x003C, // less
+                       0x3d, 0x003D, // equal
+                       0x3e, 0x003E, // greater
+                       0x3f, 0x003F, // question
+                       0x40, 0x0040, // at
+                       0x41, 0x0041, // A
+                       0x42, 0x0042, // B
+                       0x43, 0x0043, // C
+                       0x44, 0x0044, // D
+                       0x45, 0x0045, // E
+                       0x46, 0x0046, // F
+                       0x47, 0x0047, // G
+                       0x48, 0x0048, // H
+                       0x49, 0x0049, // I
+                       0x4a, 0x004A, // J
+                       0x4b, 0x004B, // K
+                       0x4c, 0x004C, // L
+                       0x4d, 0x004D, // M
+                       0x4e, 0x004E, // N
+                       0x4f, 0x004F, // O
+                       0x50, 0x0050, // P
+                       0x51, 0x0051, // Q
+                       0x52, 0x0052, // R
+                       0x53, 0x0053, // S
+                       0x54, 0x0054, // T
+                       0x55, 0x0055, // U
+                       0x56, 0x0056, // V
+                       0x57, 0x0057, // W
+                       0x58, 0x0058, // X
+                       0x59, 0x0059, // Y
+                       0x5a, 0x005A, // Z
+                       0x5b, 0x005B, // bracketleft
+                       0x5c, 0x005C, // backslash
+                       0x5d, 0x005D, // bracketright
+                       0x5e, 0x005E, // asciicircum
+                       0x5f, 0x005F, // underscore
+                       0x60, 0x0060, // grave
+                       0x61, 0x0061, // a
+                       0x62, 0x0062, // b
+                       0x63, 0x0063, // c
+                       0x64, 0x0064, // d
+                       0x65, 0x0065, // e
+                       0x66, 0x0066, // f
+                       0x67, 0x0067, // g
+                       0x68, 0x0068, // h
+                       0x69, 0x0069, // i
+                       0x6a, 0x006A, // j
+                       0x6b, 0x006B, // k
+                       0x6c, 0x006C, // l
+                       0x6d, 0x006D, // m
+                       0x6e, 0x006E, // n
+                       0x6f, 0x006F, // o
+                       0x70, 0x0070, // p
+                       0x71, 0x0071, // q
+                       0x72, 0x0072, // r
+                       0x73, 0x0073, // s
+                       0x74, 0x0074, // t
+                       0x75, 0x0075, // u
+                       0x76, 0x0076, // v
+                       0x77, 0x0077, // w
+                       0x78, 0x0078, // x
+                       0x79, 0x0079, // y
+                       0x7a, 0x007A, // z
+                       0x7b, 0x007B, // braceleft
+                       0x7c, 0x007C, // bar
+                       0x7d, 0x007D, // braceright
+                       0x7e, 0x007E, // asciitilde
+                       0x82, 0x201A, // quotesinglbase
+                       0x84, 0x201E, // quotedblbase
+                       0x85, 0x2026, // ellipsis
+                       0x86, 0x2020, // dagger
+                       0x87, 0x2021, // daggerdbl
+                       0x89, 0x2030, // perthousand
+                       0x8a, 0x0160, // Scaron
+                       0x8b, 0x2039, // guilsinglleft
+                       0x8c, 0x015A, // Sacute
+                       0x8d, 0x0164, // Tcaron
+                       0x8e, 0x017D, // Zcaron
+                       0x8f, 0x0179, // Zacute
+                       0x91, 0x2018, // quoteleft
+                       0x92, 0x2019, // quoteright
+                       0x93, 0x201C, // quotedblleft
+                       0x94, 0x201D, // quotedblright
+                       0x95, 0x2022, // bullet
+                       0x96, 0x2013, // endash
+                       0x97, 0x2014, // emdash
+                       0x99, 0x2122, // trademark
+                       0x9a, 0x0161, // scaron
+                       0x9b, 0x203A, // guilsinglright
+                       0x9c, 0x015B, // sacute
+                       0x9d, 0x0165, // tcaron
+                       0x9e, 0x017E, // zcaron
+                       0x9f, 0x017A, // zacute
+                       0xa1, 0x02C7, // caron
+                       0xa2, 0x02D8, // breve
+                       0xa3, 0x0141, // Lslash
+                       0xa4, 0x00A4, // currency
+                       0xa5, 0x0104, // Aogonek
+                       0xa6, 0x00A6, // brokenbar
+                       0xa7, 0x00A7, // section
+                       0xa8, 0x00A8, // dieresis
+                       0xa9, 0x00A9, // copyright
+                       0xaa, 0x0218, // Scommaaccent
+                       0xab, 0x00AB, // guillemotleft
+                       0xac, 0x00AC, // logicalnot
+                       0xae, 0x00AE, // registered
+                       0xaf, 0x017B, // Zdotaccent
+                       0xb0, 0x00B0, // degree
+                       0xb1, 0x00B1, // plusminus
+                       0xb2, 0x02DB, // ogonek
+                       0xb3, 0x0142, // lslash
+                       0xb4, 0x00B4, // acute
+                       0xb5, 0x00B5, // mu
+                       0xb5, 0x03BC, // mu
+                       0xb6, 0x00B6, // paragraph
+                       0xb7, 0x00B7, // periodcentered
+                       0xb7, 0x2219, // periodcentered
+                       0xb8, 0x00B8, // cedilla
+                       0xb9, 0x0105, // aogonek
+                       0xba, 0x0219, // scommaaccent
+                       0xbb, 0x00BB, // guillemotright
+                       0xbc, 0x013D, // Lcaron
+                       0xbd, 0x02DD, // hungarumlaut
+                       0xbe, 0x013E, // lcaron
+                       0xbf, 0x017C, // zdotaccent
+                       0xc0, 0x0154, // Racute
+                       0xc1, 0x00C1, // Aacute
+                       0xc2, 0x00C2, // Acircumflex
+                       0xc3, 0x0102, // Abreve
+                       0xc4, 0x00C4, // Adieresis
+                       0xc5, 0x0139, // Lacute
+                       0xc6, 0x0106, // Cacute
+                       0xc7, 0x00C7, // Ccedilla
+                       0xc8, 0x010C, // Ccaron
+                       0xc9, 0x00C9, // Eacute
+                       0xca, 0x0118, // Eogonek
+                       0xcb, 0x00CB, // Edieresis
+                       0xcc, 0x011A, // Ecaron
+                       0xcd, 0x00CD, // Iacute
+                       0xce, 0x00CE, // Icircumflex
+                       0xcf, 0x010E, // Dcaron
+                       0xd0, 0x0110, // Dcroat
+                       0xd1, 0x0143, // Nacute
+                       0xd2, 0x0147, // Ncaron
+                       0xd3, 0x00D3, // Oacute
+                       0xd4, 0x00D4, // Ocircumflex
+                       0xd5, 0x0150, // Ohungarumlaut
+                       0xd6, 0x00D6, // Odieresis
+                       0xd7, 0x00D7, // multiply
+                       0xd8, 0x0158, // Rcaron
+                       0xd9, 0x016E, // Uring
+                       0xda, 0x00DA, // Uacute
+                       0xdb, 0x0170, // Uhungarumlaut
+                       0xdc, 0x00DC, // Udieresis
+                       0xdd, 0x00DD, // Yacute
+                       0xde, 0x0162, // Tcommaaccent
+                       0xde, 0x021A, // Tcommaaccent
+                       0xdf, 0x00DF, // germandbls
+                       0xe0, 0x0155, // racute
+                       0xe1, 0x00E1, // aacute
+                       0xe2, 0x00E2, // acircumflex
+                       0xe3, 0x0103, // abreve
+                       0xe4, 0x00E4, // adieresis
+                       0xe5, 0x013A, // lacute
+                       0xe6, 0x0107, // cacute
+                       0xe7, 0x00E7, // ccedilla
+                       0xe8, 0x010D, // ccaron
+                       0xe9, 0x00E9, // eacute
+                       0xea, 0x0119, // eogonek
+                       0xeb, 0x00EB, // edieresis
+                       0xec, 0x011B, // ecaron
+                       0xed, 0x00ED, // iacute
+                       0xee, 0x00EE, // icircumflex
+                       0xef, 0x010F, // dcaron
+                       0xf0, 0x0111, // dcroat
+                       0xf1, 0x0144, // nacute
+                       0xf2, 0x0148, // ncaron
+                       0xf3, 0x00F3, // oacute
+                       0xf4, 0x00F4, // ocircumflex
+                       0xf5, 0x0151, // ohungarumlaut
+                       0xf6, 0x00F6, // odieresis
+                       0xf7, 0x00F7, // divide
+                       0xf8, 0x0159, // rcaron
+                       0xf9, 0x016F, // uring
+                       0xfa, 0x00FA, // uacute
+                       0xfb, 0x0171, // uhungarumlaut
+                       0xfc, 0x00FC, // udieresis
+                       0xfd, 0x00FD, // yacute
+                       0xfe, 0x0163, // tcommaaccent
+                       0xfe, 0x021B, // tcommaaccent
+                       0xff, 0x02D9, // dotaccent
+       };
+
+       private static final int[] encMacRomanEncoding
+                       = {
+                       0x20, 0x0020, // space
+                       0x20, 0x00A0, // space
+                       0x21, 0x0021, // exclam
+                       0x22, 0x0022, // quotedbl
+                       0x23, 0x0023, // numbersign
+                       0x24, 0x0024, // dollar
+                       0x25, 0x0025, // percent
+                       0x26, 0x0026, // ampersand
+                       0x27, 0x0027, // quotesingle
+                       0x28, 0x0028, // parenleft
+                       0x29, 0x0029, // parenright
+                       0x2a, 0x002A, // asterisk
+                       0x2b, 0x002B, // plus
+                       0x2c, 0x002C, // comma
+                       0x2d, 0x002D, // hyphen
+                       0x2d, 0x00AD, // hyphen
+                       0x2e, 0x002E, // period
+                       0x2f, 0x002F, // slash
+                       0x30, 0x0030, // zero
+                       0x31, 0x0031, // one
+                       0x32, 0x0032, // two
+                       0x33, 0x0033, // three
+                       0x34, 0x0034, // four
+                       0x35, 0x0035, // five
+                       0x36, 0x0036, // six
+                       0x37, 0x0037, // seven
+                       0x38, 0x0038, // eight
+                       0x39, 0x0039, // nine
+                       0x3a, 0x003A, // colon
+                       0x3b, 0x003B, // semicolon
+                       0x3c, 0x003C, // less
+                       0x3d, 0x003D, // equal
+                       0x3e, 0x003E, // greater
+                       0x3f, 0x003F, // question
+                       0x40, 0x0040, // at
+                       0x41, 0x0041, // A
+                       0x42, 0x0042, // B
+                       0x43, 0x0043, // C
+                       0x44, 0x0044, // D
+                       0x45, 0x0045, // E
+                       0x46, 0x0046, // F
+                       0x47, 0x0047, // G
+                       0x48, 0x0048, // H
+                       0x49, 0x0049, // I
+                       0x4a, 0x004A, // J
+                       0x4b, 0x004B, // K
+                       0x4c, 0x004C, // L
+                       0x4d, 0x004D, // M
+                       0x4e, 0x004E, // N
+                       0x4f, 0x004F, // O
+                       0x50, 0x0050, // P
+                       0x51, 0x0051, // Q
+                       0x52, 0x0052, // R
+                       0x53, 0x0053, // S
+                       0x54, 0x0054, // T
+                       0x55, 0x0055, // U
+                       0x56, 0x0056, // V
+                       0x57, 0x0057, // W
+                       0x58, 0x0058, // X
+                       0x59, 0x0059, // Y
+                       0x5a, 0x005A, // Z
+                       0x5b, 0x005B, // bracketleft
+                       0x5c, 0x005C, // backslash
+                       0x5d, 0x005D, // bracketright
+                       0x5e, 0x005E, // asciicircum
+                       0x5f, 0x005F, // underscore
+                       0x60, 0x0060, // grave
+                       0x61, 0x0061, // a
+                       0x62, 0x0062, // b
+                       0x63, 0x0063, // c
+                       0x64, 0x0064, // d
+                       0x65, 0x0065, // e
+                       0x66, 0x0066, // f
+                       0x67, 0x0067, // g
+                       0x68, 0x0068, // h
+                       0x69, 0x0069, // i
+                       0x6a, 0x006A, // j
+                       0x6b, 0x006B, // k
+                       0x6c, 0x006C, // l
+                       0x6d, 0x006D, // m
+                       0x6e, 0x006E, // n
+                       0x6f, 0x006F, // o
+                       0x70, 0x0070, // p
+                       0x71, 0x0071, // q
+                       0x72, 0x0072, // r
+                       0x73, 0x0073, // s
+                       0x74, 0x0074, // t
+                       0x75, 0x0075, // u
+                       0x76, 0x0076, // v
+                       0x77, 0x0077, // w
+                       0x78, 0x0078, // x
+                       0x79, 0x0079, // y
+                       0x7b, 0x007B, // braceleft
+                       0x7c, 0x007C, // bar
+                       0x7d, 0x007D, // braceright
+                       0x7e, 0x007E, // asciitilde
+                       0x80, 0x00C4, // Adieresis
+                       0x81, 0x00C5, // Aring
+                       0x82, 0x00C7, // Ccedilla
+                       0x83, 0x00C9, // Eacute
+                       0x84, 0x00D1, // Ntilde
+                       0x85, 0x00D6, // Odieresis
+                       0x86, 0x00DC, // Udieresis
+                       0x87, 0x00E1, // aacute
+                       0x88, 0x00E0, // agrave
+                       0x89, 0x00E2, // acircumflex
+                       0x8a, 0x00E4, // adieresis
+                       0x8b, 0x00E3, // atilde
+                       0x8c, 0x00E5, // aring
+                       0x8d, 0x00E7, // ccedilla
+                       0x8e, 0x00E9, // eacute
+                       0x8f, 0x00E8, // egrave
+                       0x90, 0x00EA, // ecircumflex
+                       0x91, 0x00EB, // edieresis
+                       0x92, 0x00ED, // iacute
+                       0x93, 0x00EC, // igrave
+                       0x94, 0x00EE, // icircumflex
+                       0x95, 0x00EF, // idieresis
+                       0x96, 0x00F1, // ntilde
+                       0x97, 0x00F3, // oacute
+                       0x98, 0x00F2, // ograve
+                       0x99, 0x00F4, // ocircumflex
+                       0x9a, 0x00F6, // odieresis
+                       0x9b, 0x00F5, // otilde
+                       0x9c, 0x00FA, // uacute
+                       0x9d, 0x00F9, // ugrave
+                       0x9e, 0x00FB, // ucircumflex
+                       0x9f, 0x00FC, // udieresis
+                       0xa0, 0x2020, // dagger
+                       0xa1, 0x00B0, // degree
+                       0xa2, 0x00A2, // cent
+                       0xa3, 0x00A3, // sterling
+                       0xa4, 0x00A7, // section
+                       0xa5, 0x2022, // bullet
+                       0xa6, 0x00B6, // paragraph
+                       0xa7, 0x00DF, // germandbls
+                       0xa8, 0x00AE, // registered
+                       0xa9, 0x00A9, // copyright
+                       0xaa, 0x2122, // trademark
+                       0xab, 0x00B4, // acute
+                       0xac, 0x00A8, // dieresis
+                       0xae, 0x00C6, // AE
+                       0xaf, 0x00D8, // Oslash
+                       0xb1, 0x00B1, // plusminus
+                       0xb5, 0x00B5, // mu
+                       0xb5, 0x03BC, // mu
+                       0xbb, 0x00AA, // ordfeminine
+                       0xbc, 0x00BA, // ordmasculine
+                       0xbe, 0x00E6, // ae
+                       0xbf, 0x00F8, // oslash
+                       0xc0, 0x00BF, // questiondown
+                       0xc1, 0x00A1, // exclamdown
+                       0xc2, 0x00AC, // logicalnot
+                       0xc4, 0x0192, // florin
+                       0xc7, 0x00AB, // guillemotleft
+                       0xc8, 0x00BB, // guillemotright
+                       0xc9, 0x2026, // ellipsis
+                       0xcb, 0x00C0, // Agrave
+                       0xcc, 0x00C3, // Atilde
+                       0xcd, 0x00D5, // Otilde
+                       0xce, 0x0152, // OE
+                       0xcf, 0x0153, // oe
+                       0xd0, 0x2013, // endash
+                       0xd1, 0x2014, // emdash
+                       0xd2, 0x201C, // quotedblleft
+                       0xd3, 0x201D, // quotedblright
+                       0xd4, 0x2018, // quoteleft
+                       0xd5, 0x2019, // quoteright
+                       0xd6, 0x00F7, // divide
+                       0xd9, 0x0178, // Ydieresis
+                       0xda, 0x2044, // fraction
+                       0xda, 0x2215, // fraction
+                       0xdb, 0x00A4, // currency
+                       0xdc, 0x2039, // guilsinglleft
+                       0xdd, 0x203A, // guilsinglright
+                       0xde, 0xFB01, // fi
+                       0xdf, 0xFB02, // fl
+                       0xe0, 0x2021, // daggerdbl
+                       0xe1, 0x00B7, // periodcentered
+                       0xe1, 0x2219, // periodcentered
+                       0xe2, 0x201A, // quotesinglbase
+                       0xe3, 0x201E, // quotedblbase
+                       0xe4, 0x2030, // perthousand
+                       0xe5, 0x00C2, // Acircumflex
+                       0xe6, 0x00CA, // Ecircumflex
+                       0xe7, 0x00C1, // Aacute
+                       0xe8, 0x00CB, // Edieresis
+                       0xe9, 0x00C8, // Egrave
+                       0xea, 0x00CD, // Iacute
+                       0xeb, 0x00CE, // Icircumflex
+                       0xec, 0x00CF, // Idieresis
+                       0xed, 0x00CC, // Igrave
+                       0xee, 0x00D3, // Oacute
+                       0xef, 0x00D4, // Ocircumflex
+                       0xf1, 0x00D2, // Ograve
+                       0xf2, 0x00DA, // Uacute
+                       0xf3, 0x00DB, // Ucircumflex
+                       0xf4, 0x00D9, // Ugrave
+                       0xf5, 0x0131, // dotlessi
+                       0xf6, 0x02C6, // circumflex
+                       0xf7, 0x02DC, // tilde
+                       0xf8, 0x00AF, // macron
+                       0xf8, 0x02C9, // macron
+                       0xf9, 0x02D8, // breve
+                       0xfa, 0x02D9, // dotaccent
+                       0xfb, 0x02DA, // ring
+                       0xfc, 0x00B8, // cedilla
+                       0xfd, 0x02DD, // hungarumlaut
+                       0xfe, 0x02DB, // ogonek
+                       0xff, 0x02C7, // caron
+                       0xd8, 0x00FF, // ydieresis
+                       0xb4, 0x00A5, // yen
+                       0x7a, 0x007A, // z
+       };
+
+       private static final int[] encWinAnsiEncoding
+                       = {
+                       0x20, 0x0020, // space
+                       0x20, 0x00A0, // space
+                       0x21, 0x0021, // exclam
+                       0x22, 0x0022, // quotedbl
+                       0x23, 0x0023, // numbersign
+                       0x24, 0x0024, // dollar
+                       0x25, 0x0025, // percent
+                       0x26, 0x0026, // ampersand
+                       0x27, 0x0027, // quotesingle
+                       0x28, 0x0028, // parenleft
+                       0x29, 0x0029, // parenright
+                       0x2a, 0x002A, // asterisk
+                       0x2b, 0x002B, // plus
+                       0x2c, 0x002C, // comma
+                       0x2d, 0x002D, // hyphen
+                       0x2d, 0x00AD, // hyphen
+                       0x2e, 0x002E, // period
+                       0x2f, 0x002F, // slash
+                       0x30, 0x0030, // zero
+                       0x31, 0x0031, // one
+                       0x32, 0x0032, // two
+                       0x33, 0x0033, // three
+                       0x34, 0x0034, // four
+                       0x35, 0x0035, // five
+                       0x36, 0x0036, // six
+                       0x37, 0x0037, // seven
+                       0x38, 0x0038, // eight
+                       0x39, 0x0039, // nine
+                       0x3a, 0x003A, // colon
+                       0x3b, 0x003B, // semicolon
+                       0x3c, 0x003C, // less
+                       0x3d, 0x003D, // equal
+                       0x3e, 0x003E, // greater
+                       0x3f, 0x003F, // question
+                       0x40, 0x0040, // at
+                       0x41, 0x0041, // A
+                       0x42, 0x0042, // B
+                       0x43, 0x0043, // C
+                       0x44, 0x0044, // D
+                       0x45, 0x0045, // E
+                       0x46, 0x0046, // F
+                       0x47, 0x0047, // G
+                       0x48, 0x0048, // H
+                       0x49, 0x0049, // I
+                       0x4a, 0x004A, // J
+                       0x4b, 0x004B, // K
+                       0x4c, 0x004C, // L
+                       0x4d, 0x004D, // M
+                       0x4e, 0x004E, // N
+                       0x4f, 0x004F, // O
+                       0x50, 0x0050, // P
+                       0x51, 0x0051, // Q
+                       0x52, 0x0052, // R
+                       0x53, 0x0053, // S
+                       0x54, 0x0054, // T
+                       0x55, 0x0055, // U
+                       0x56, 0x0056, // V
+                       0x57, 0x0057, // W
+                       0x58, 0x0058, // X
+                       0x59, 0x0059, // Y
+                       0x5a, 0x005A, // Z
+                       0x5b, 0x005B, // bracketleft
+                       0x5c, 0x005C, // backslash
+                       0x5d, 0x005D, // bracketright
+                       0x5e, 0x005E, // asciicircum
+                       0x5f, 0x005F, // underscore
+                       0x60, 0x0060, // grave
+                       0x61, 0x0061, // a
+                       0x62, 0x0062, // b
+                       0x63, 0x0063, // c
+                       0x64, 0x0064, // d
+                       0x65, 0x0065, // e
+                       0x66, 0x0066, // f
+                       0x67, 0x0067, // g
+                       0x68, 0x0068, // h
+                       0x69, 0x0069, // i
+                       0x6a, 0x006A, // j
+                       0x6b, 0x006B, // k
+                       0x6c, 0x006C, // l
+                       0x6d, 0x006D, // m
+                       0x6e, 0x006E, // n
+                       0x6f, 0x006F, // o
+                       0x70, 0x0070, // p
+                       0x71, 0x0071, // q
+                       0x72, 0x0072, // r
+                       0x73, 0x0073, // s
+                       0x74, 0x0074, // t
+                       0x75, 0x0075, // u
+                       0x76, 0x0076, // v
+                       0x77, 0x0077, // w
+                       0x78, 0x0078, // x
+                       0x79, 0x0079, // y
+                       0x7a, 0x007A, // z
+                       0x7b, 0x007B, // braceleft
+                       0x7c, 0x007C, // bar
+                       0x7d, 0x007D, // braceright
+                       0x7e, 0x007E, // asciitilde
+                       0x80, 0x20AC, // Euro
+                       0x82, 0x201A, // quotesinglbase
+                       0x83, 0x0192, // florin
+                       0x84, 0x201E, // quotedblbase
+                       0x85, 0x2026, // ellipsis
+                       0x86, 0x2020, // dagger
+                       0x87, 0x2021, // daggerdbl
+                       0x88, 0x02C6, // circumflex
+                       0x89, 0x2030, // perthousand
+                       0x8a, 0x0160, // Scaron
+                       0x8b, 0x2039, // guilsinglleft
+                       0x8c, 0x0152, // OE
+                       0x8e, 0x017D, // Zcaron
+                       0x91, 0x2018, // quoteleft
+                       0x92, 0x2019, // quoteright
+                       0x93, 0x201C, // quotedblleft
+                       0x94, 0x201D, // quotedblright
+                       0x95, 0x2022, // bullet
+                       0x96, 0x2013, // endash
+                       0x97, 0x2014, // emdash
+                       0x98, 0x02DC, // tilde
+                       0x99, 0x2122, // trademark
+                       0x9a, 0x0161, // scaron
+                       0x9b, 0x203A, // guilsinglright
+                       0x9c, 0x0153, // oe
+                       0x9e, 0x017E, // zcaron
+                       0x9f, 0x0178, // Ydieresis
+                       0xa1, 0x00A1, // exclamdown
+                       0xa2, 0x00A2, // cent
+                       0xa3, 0x00A3, // sterling
+                       0xa4, 0x00A4, // currency
+                       0xa5, 0x00A5, // yen
+                       0xa6, 0x00A6, // brokenbar
+                       0xa7, 0x00A7, // section
+                       0xa8, 0x00A8, // dieresis
+                       0xa9, 0x00A9, // copyright
+                       0xaa, 0x00AA, // ordfeminine
+                       0xab, 0x00AB, // guillemotleft
+                       0xac, 0x00AC, // logicalnot
+                       0xae, 0x00AE, // registered
+                       0xaf, 0x00AF, // macron
+                       0xaf, 0x02C9, // macron
+                       0xb0, 0x00B0, // degree
+                       0xb1, 0x00B1, // plusminus
+                       0xb2, 0x00B2, // twosuperior
+                       0xb3, 0x00B3, // threesuperior
+                       0xb4, 0x00B4, // acute
+                       0xb5, 0x00B5, // mu
+                       0xb5, 0x03BC, // mu
+                       0xb6, 0x00B6, // paragraph
+                       0xb7, 0x00B7, // periodcentered
+                       0xb7, 0x2219, // periodcentered
+                       0xb8, 0x00B8, // cedilla
+                       0xb9, 0x00B9, // onesuperior
+                       0xba, 0x00BA, // ordmasculine
+                       0xbb, 0x00BB, // guillemotright
+                       0xbc, 0x00BC, // onequarter
+                       0xbd, 0x00BD, // onehalf
+                       0xbe, 0x00BE, // threequarters
+                       0xbf, 0x00BF, // questiondown
+                       0xc0, 0x00C0, // Agrave
+                       0xc1, 0x00C1, // Aacute
+                       0xc2, 0x00C2, // Acircumflex
+                       0xc3, 0x00C3, // Atilde
+                       0xc4, 0x00C4, // Adieresis
+                       0xc5, 0x00C5, // Aring
+                       0xc6, 0x00C6, // AE
+                       0xc7, 0x00C7, // Ccedilla
+                       0xc8, 0x00C8, // Egrave
+                       0xc9, 0x00C9, // Eacute
+                       0xca, 0x00CA, // Ecircumflex
+                       0xcb, 0x00CB, // Edieresis
+                       0xcc, 0x00CC, // Igrave
+                       0xcd, 0x00CD, // Iacute
+                       0xce, 0x00CE, // Icircumflex
+                       0xcf, 0x00CF, // Idieresis
+                       0xd0, 0x00D0, // Eth
+                       0xd1, 0x00D1, // Ntilde
+                       0xd2, 0x00D2, // Ograve
+                       0xd3, 0x00D3, // Oacute
+                       0xd4, 0x00D4, // Ocircumflex
+                       0xd5, 0x00D5, // Otilde
+                       0xd6, 0x00D6, // Odieresis
+                       0xd7, 0x00D7, // multiply
+                       0xd8, 0x00D8, // Oslash
+                       0xd9, 0x00D9, // Ugrave
+                       0xda, 0x00DA, // Uacute
+                       0xdb, 0x00DB, // Ucircumflex
+                       0xdc, 0x00DC, // Udieresis
+                       0xdd, 0x00DD, // Yacute
+                       0xde, 0x00DE, // Thorn
+                       0xdf, 0x00DF, // germandbls
+                       0xe0, 0x00E0, // agrave
+                       0xe1, 0x00E1, // aacute
+                       0xe2, 0x00E2, // acircumflex
+                       0xe3, 0x00E3, // atilde
+                       0xe4, 0x00E4, // adieresis
+                       0xe5, 0x00E5, // aring
+                       0xe6, 0x00E6, // ae
+                       0xe7, 0x00E7, // ccedilla
+                       0xe8, 0x00E8, // egrave
+                       0xe9, 0x00E9, // eacute
+                       0xea, 0x00EA, // ecircumflex
+                       0xeb, 0x00EB, // edieresis
+                       0xec, 0x00EC, // igrave
+                       0xed, 0x00ED, // iacute
+                       0xee, 0x00EE, // icircumflex
+                       0xef, 0x00EF, // idieresis
+                       0xf0, 0x00F0, // eth
+                       0xf1, 0x00F1, // ntilde
+                       0xf2, 0x00F2, // ograve
+                       0xf3, 0x00F3, // oacute
+                       0xf4, 0x00F4, // ocircumflex
+                       0xf5, 0x00F5, // otilde
+                       0xf6, 0x00F6, // odieresis
+                       0xf7, 0x00F7, // divide
+                       0xf8, 0x00F8, // oslash
+                       0xf9, 0x00F9, // ugrave
+                       0xfa, 0x00FA, // uacute
+                       0xfb, 0x00FB, // ucircumflex
+                       0xfc, 0x00FC, // udieresis
+                       0xfd, 0x00FD, // yacute
+                       0xfe, 0x00FE, // thorn
+                       0xff, 0x00FF, // ydieresis
+       };
+
+       private static final int[] encPDFDocEncoding
+                       = {
+                       0x18, 0x02D8, // breve
+                       0x19, 0x02C7, // caron
+                       0x1a, 0x02C6, // circumflex
+                       0x1b, 0x02D9, // dotaccent
+                       0x1c, 0x02DD, // hungarumlaut
+                       0x1d, 0x02DB, // ogonek
+                       0x1e, 0x02DA, // ring
+                       0x1f, 0x02DC, // tilde
+                       0x20, 0x0020, // space
+                       0x20, 0x00A0, // space
+                       0x21, 0x0021, // exclam
+                       0x22, 0x0022, // quotedbl
+                       0x23, 0x0023, // numbersign
+                       0x24, 0x0024, // dollar
+                       0x25, 0x0025, // percent
+                       0x26, 0x0026, // ampersand
+                       0x27, 0x0027, // quotesingle
+                       0x28, 0x0028, // parenleft
+                       0x29, 0x0029, // parenright
+                       0x2a, 0x002A, // asterisk
+                       0x2b, 0x002B, // plus
+                       0x2c, 0x002C, // comma
+                       0x2d, 0x002D, // hyphen
+                       0x2d, 0x00AD, // hyphen
+                       0x2e, 0x002E, // period
+                       0x2f, 0x002F, // slash
+                       0x30, 0x0030, // zero
+                       0x31, 0x0031, // one
+                       0x32, 0x0032, // two
+                       0x33, 0x0033, // three
+                       0x34, 0x0034, // four
+                       0x35, 0x0035, // five
+                       0x36, 0x0036, // six
+                       0x37, 0x0037, // seven
+                       0x38, 0x0038, // eight
+                       0x39, 0x0039, // nine
+                       0x3a, 0x003A, // colon
+                       0x3b, 0x003B, // semicolon
+                       0x3c, 0x003C, // less
+                       0x3d, 0x003D, // equal
+                       0x3e, 0x003E, // greater
+                       0x3f, 0x003F, // question
+                       0x40, 0x0040, // at
+                       0x41, 0x0041, // A
+                       0x42, 0x0042, // B
+                       0x43, 0x0043, // C
+                       0x44, 0x0044, // D
+                       0x45, 0x0045, // E
+                       0x46, 0x0046, // F
+                       0x47, 0x0047, // G
+                       0x48, 0x0048, // H
+                       0x49, 0x0049, // I
+                       0x4a, 0x004A, // J
+                       0x4b, 0x004B, // K
+                       0x4c, 0x004C, // L
+                       0x4d, 0x004D, // M
+                       0x4e, 0x004E, // N
+                       0x4f, 0x004F, // O
+                       0x50, 0x0050, // P
+                       0x51, 0x0051, // Q
+                       0x52, 0x0052, // R
+                       0x53, 0x0053, // S
+                       0x54, 0x0054, // T
+                       0x55, 0x0055, // U
+                       0x56, 0x0056, // V
+                       0x57, 0x0057, // W
+                       0x58, 0x0058, // X
+                       0x59, 0x0059, // Y
+                       0x5a, 0x005A, // Z
+                       0x5b, 0x005B, // bracketleft
+                       0x5c, 0x005C, // backslash
+                       0x5d, 0x005D, // bracketright
+                       0x5e, 0x005E, // asciicircum
+                       0x5f, 0x005F, // underscore
+                       0x60, 0x0060, // grave
+                       0x61, 0x0061, // a
+                       0x62, 0x0062, // b
+                       0x63, 0x0063, // c
+                       0x64, 0x0064, // d
+                       0x65, 0x0065, // e
+                       0x66, 0x0066, // f
+                       0x67, 0x0067, // g
+                       0x68, 0x0068, // h
+                       0x69, 0x0069, // i
+                       0x6a, 0x006A, // j
+                       0x6b, 0x006B, // k
+                       0x6c, 0x006C, // l
+                       0x6d, 0x006D, // m
+                       0x6e, 0x006E, // n
+                       0x6f, 0x006F, // o
+                       0x70, 0x0070, // p
+                       0x71, 0x0071, // q
+                       0x72, 0x0072, // r
+                       0x73, 0x0073, // s
+                       0x74, 0x0074, // t
+                       0x75, 0x0075, // u
+                       0x76, 0x0076, // v
+                       0x77, 0x0077, // w
+                       0x78, 0x0078, // x
+                       0x79, 0x0079, // y
+                       0x7a, 0x007A, // z
+                       0x7b, 0x007B, // braceleft
+                       0x7c, 0x007C, // bar
+                       0x7d, 0x007D, // braceright
+                       0x7e, 0x007E, // asciitilde
+                       0x80, 0x2022, // bullet
+                       0x81, 0x2020, // dagger
+                       0x82, 0x2021, // daggerdbl
+                       0x83, 0x2026, // ellipsis
+                       0x84, 0x2014, // emdash
+                       0x85, 0x2013, // endash
+                       0x86, 0x0192, // florin
+                       0x87, 0x2044, // fraction
+                       0x87, 0x2215, // fraction
+                       0x88, 0x2039, // guilsinglleft
+                       0x89, 0x203A, // guilsinglright
+                       0x8a, 0x2212, // minus
+                       0x8b, 0x2030, // perthousand
+                       0x8c, 0x201E, // quotedblbase
+                       0x8d, 0x201C, // quotedblleft
+                       0x8e, 0x201D, // quotedblright
+                       0x8f, 0x2018, // quoteleft
+                       0x90, 0x2019, // quoteright
+                       0x91, 0x201A, // quotesinglbase
+                       0x92, 0x2122, // trademark
+                       0x93, 0xFB01, // fi
+                       0x94, 0xFB02, // fl
+                       0x95, 0x0141, // Lslash
+                       0x96, 0x0152, // OE
+                       0x97, 0x0160, // Scaron
+                       0x98, 0x0178, // Ydieresis
+                       0x99, 0x017D, // Zcaron
+                       0x9a, 0x0131, // dotlessi
+                       0x9b, 0x0142, // lslash
+                       0x9c, 0x0153, // oe
+                       0x9d, 0x0161, // scaron
+                       0x9e, 0x017E, // zcaron
+                       0xa0, 0x20AC, // Euro
+                       0xa1, 0x00A1, // exclamdown
+                       0xa2, 0x00A2, // cent
+                       0xa3, 0x00A3, // sterling
+                       0xa4, 0x00A4, // currency
+                       0xa5, 0x00A5, // yen
+                       0xa6, 0x00A6, // brokenbar
+                       0xa7, 0x00A7, // section
+                       0xa8, 0x00A8, // dieresis
+                       0xa9, 0x00A9, // copyright
+                       0xaa, 0x00AA, // ordfeminine
+                       0xab, 0x00AB, // guillemotleft
+                       0xac, 0x00AC, // logicalnot
+                       0xae, 0x00AE, // registered
+                       0xaf, 0x00AF, // macron
+                       0xaf, 0x02C9, // macron
+                       0xb0, 0x00B0, // degree
+                       0xb1, 0x00B1, // plusminus
+                       0xb2, 0x00B2, // twosuperior
+                       0xb3, 0x00B3, // threesuperior
+                       0xb4, 0x00B4, // acute
+                       0xb5, 0x00B5, // mu
+                       0xb5, 0x03BC, // mu
+                       0xb6, 0x00B6, // paragraph
+                       0xb7, 0x00B7, // periodcentered
+                       0xb7, 0x2219, // periodcentered
+                       0xb8, 0x00B8, // cedilla
+                       0xb9, 0x00B9, // onesuperior
+                       0xba, 0x00BA, // ordmasculine
+                       0xbb, 0x00BB, // guillemotright
+                       0xbc, 0x00BC, // onequarter
+                       0xbd, 0x00BD, // onehalf
+                       0xbe, 0x00BE, // threequarters
+                       0xbf, 0x00BF, // questiondown
+                       0xc0, 0x00C0, // Agrave
+                       0xc1, 0x00C1, // Aacute
+                       0xc2, 0x00C2, // Acircumflex
+                       0xc3, 0x00C3, // Atilde
+                       0xc4, 0x00C4, // Adieresis
+                       0xc5, 0x00C5, // Aring
+                       0xc6, 0x00C6, // AE
+                       0xc7, 0x00C7, // Ccedilla
+                       0xc8, 0x00C8, // Egrave
+                       0xc9, 0x00C9, // Eacute
+                       0xca, 0x00CA, // Ecircumflex
+                       0xcb, 0x00CB, // Edieresis
+                       0xcc, 0x00CC, // Igrave
+                       0xcd, 0x00CD, // Iacute
+                       0xce, 0x00CE, // Icircumflex
+                       0xcf, 0x00CF, // Idieresis
+                       0xd0, 0x00D0, // Eth
+                       0xd1, 0x00D1, // Ntilde
+                       0xd2, 0x00D2, // Ograve
+                       0xd3, 0x00D3, // Oacute
+                       0xd4, 0x00D4, // Ocircumflex
+                       0xd5, 0x00D5, // Otilde
+                       0xd6, 0x00D6, // Odieresis
+                       0xd7, 0x00D7, // multiply
+                       0xd8, 0x00D8, // Oslash
+                       0xd9, 0x00D9, // Ugrave
+                       0xda, 0x00DA, // Uacute
+                       0xdb, 0x00DB, // Ucircumflex
+                       0xdc, 0x00DC, // Udieresis
+                       0xdd, 0x00DD, // Yacute
+                       0xde, 0x00DE, // Thorn
+                       0xdf, 0x00DF, // germandbls
+                       0xe0, 0x00E0, // agrave
+                       0xe1, 0x00E1, // aacute
+                       0xe2, 0x00E2, // acircumflex
+                       0xe3, 0x00E3, // atilde
+                       0xe4, 0x00E4, // adieresis
+                       0xe5, 0x00E5, // aring
+                       0xe6, 0x00E6, // ae
+                       0xe7, 0x00E7, // ccedilla
+                       0xe8, 0x00E8, // egrave
+                       0xe9, 0x00E9, // eacute
+                       0xea, 0x00EA, // ecircumflex
+                       0xeb, 0x00EB, // edieresis
+                       0xec, 0x00EC, // igrave
+                       0xed, 0x00ED, // iacute
+                       0xee, 0x00EE, // icircumflex
+                       0xef, 0x00EF, // idieresis
+                       0xf0, 0x00F0, // eth
+                       0xf1, 0x00F1, // ntilde
+                       0xf2, 0x00F2, // ograve
+                       0xf3, 0x00F3, // oacute
+                       0xf4, 0x00F4, // ocircumflex
+                       0xf5, 0x00F5, // otilde
+                       0xf6, 0x00F6, // odieresis
+                       0xf7, 0x00F7, // divide
+                       0xf8, 0x00F8, // oslash
+                       0xf9, 0x00F9, // ugrave
+                       0xfa, 0x00FA, // uacute
+                       0xfb, 0x00FB, // ucircumflex
+                       0xfc, 0x00FC, // udieresis
+                       0xfd, 0x00FD, // yacute
+                       0xfe, 0x00FE, // thorn
+                       0xff, 0x00FF, // ydieresis
+       };
+
+       private static final int[] encSymbolEncoding
+                       = {
+                       0x20, 0x0020, // space
+                       0x20, 0x00A0, // space
+                       0x21, 0x0021, // exclam
+                       0x22, 0x2200, // universal
+                       0x23, 0x0023, // numbersign
+                       0x24, 0x2203, // existential
+                       0x25, 0x0025, // percent
+                       0x26, 0x0026, // ampersand
+                       0x27, 0x220B, // suchthat
+                       0x28, 0x0028, // parenleft
+                       0x29, 0x0029, // parenright
+                       0x2a, 0x2217, // asteriskmath
+                       0x2b, 0x002B, // plus
+                       0x2c, 0x002C, // comma
+                       0x2d, 0x2212, // minus
+                       0x2e, 0x002E, // period
+                       0x2f, 0x002F, // slash
+                       0x30, 0x0030, // zero
+                       0x31, 0x0031, // one
+                       0x32, 0x0032, // two
+                       0x33, 0x0033, // three
+                       0x34, 0x0034, // four
+                       0x35, 0x0035, // five
+                       0x36, 0x0036, // six
+                       0x37, 0x0037, // seven
+                       0x38, 0x0038, // eight
+                       0x39, 0x0039, // nine
+                       0x3a, 0x003A, // colon
+                       0x3b, 0x003B, // semicolon
+                       0x3c, 0x003C, // less
+                       0x3d, 0x003D, // equal
+                       0x3e, 0x003E, // greater
+                       0x3f, 0x003F, // question
+                       0x40, 0x2245, // congruent
+                       0x41, 0x0391, // Alpha
+                       0x42, 0x0392, // Beta
+                       0x43, 0x03A7, // Chi
+                       0x44, 0x2206, // Delta
+                       0x44, 0x0394, // Delta
+                       0x45, 0x0395, // Epsilon
+                       0x46, 0x03A6, // Phi
+                       0x47, 0x0393, // Gamma
+                       0x48, 0x0397, // Eta
+                       0x49, 0x0399, // Iota
+                       0x4a, 0x03D1, // theta1
+                       0x4b, 0x039A, // Kappa
+                       0x4c, 0x039B, // Lambda
+                       0x4d, 0x039C, // Mu
+                       0x4e, 0x039D, // Nu
+                       0x4f, 0x039F, // Omicron
+                       0x50, 0x03A0, // Pi
+                       0x51, 0x0398, // Theta
+                       0x52, 0x03A1, // Rho
+                       0x53, 0x03A3, // Sigma
+                       0x54, 0x03A4, // Tau
+                       0x55, 0x03A5, // Upsilon
+                       0x56, 0x03C2, // sigma1
+                       0x57, 0x2126, // Omega
+                       0x57, 0x03A9, // Omega
+                       0x58, 0x039E, // Xi
+                       0x59, 0x03A8, // Psi
+                       0x5a, 0x0396, // Zeta
+                       0x5b, 0x005B, // bracketleft
+                       0x5c, 0x2234, // therefore
+                       0x5d, 0x005D, // bracketright
+                       0x5e, 0x22A5, // perpendicular
+                       0x5f, 0x005F, // underscore
+                       0x60, 0xF8E5, // radicalex
+                       0x61, 0x03B1, // alpha
+                       0x62, 0x03B2, // beta
+                       0x63, 0x03C7, // chi
+                       0x64, 0x03B4, // delta
+                       0x65, 0x03B5, // epsilon
+                       0x66, 0x03C6, // phi
+                       0x67, 0x03B3, // gamma
+                       0x68, 0x03B7, // eta
+                       0x69, 0x03B9, // iota
+                       0x6a, 0x03D5, // phi1
+                       0x6b, 0x03BA, // kappa
+                       0x6c, 0x03BB, // lambda
+                       0x6d, 0x00B5, // mu
+                       0x6d, 0x03BC, // mu
+                       0x6e, 0x03BD, // nu
+                       0x6f, 0x03BF, // omicron
+                       0x70, 0x03C0, // pi
+                       0x71, 0x03B8, // theta
+                       0x72, 0x03C1, // rho
+                       0x73, 0x03C3, // sigma
+                       0x74, 0x03C4, // tau
+                       0x75, 0x03C5, // upsilon
+                       0x76, 0x03D6, // omega1
+                       0x77, 0x03C9, // omega
+                       0x78, 0x03BE, // xi
+                       0x79, 0x03C8, // psi
+                       0x7a, 0x03B6, // zeta
+                       0x7b, 0x007B, // braceleft
+                       0x7c, 0x007C, // bar
+                       0x7d, 0x007D, // braceright
+                       0x7e, 0x223C, // similar
+                       0xa0, 0x20AC, // Euro
+                       0xa1, 0x03D2, // Upsilon1
+                       0xa2, 0x2032, // minute
+                       0xa3, 0x2264, // lessequal
+                       0xa4, 0x2044, // fraction
+                       0xa4, 0x2215, // fraction
+                       0xa5, 0x221E, // infinity
+                       0xa6, 0x0192, // florin
+                       0xa7, 0x2663, // club
+                       0xa8, 0x2666, // diamond
+                       0xa9, 0x2665, // heart
+                       0xaa, 0x2660, // spade
+                       0xab, 0x2194, // arrowboth
+                       0xac, 0x2190, // arrowleft
+                       0xad, 0x2191, // arrowup
+                       0xae, 0x2192, // arrowright
+                       0xaf, 0x2193, // arrowdown
+                       0xb0, 0x00B0, // degree
+                       0xb1, 0x00B1, // plusminus
+                       0xb2, 0x2033, // second
+                       0xb3, 0x2265, // greaterequal
+                       0xb4, 0x00D7, // multiply
+                       0xb5, 0x221D, // proportional
+                       0xb6, 0x2202, // partialdiff
+                       0xb7, 0x2022, // bullet
+                       0xb8, 0x00F7, // divide
+                       0xb9, 0x2260, // notequal
+                       0xba, 0x2261, // equivalence
+                       0xbb, 0x2248, // approxequal
+                       0xbc, 0x2026, // ellipsis
+                       0xbd, 0xF8E6, // arrowvertex
+                       0xbe, 0xF8E7, // arrowhorizex
+                       0xbf, 0x21B5, // carriagereturn
+                       0xc0, 0x2135, // aleph
+                       0xc1, 0x2111, // Ifraktur
+                       0xc2, 0x211C, // Rfraktur
+                       0xc3, 0x2118, // weierstrass
+                       0xc4, 0x2297, // circlemultiply
+                       0xc5, 0x2295, // circleplus
+                       0xc6, 0x2205, // emptyset
+                       0xc7, 0x2229, // intersection
+                       0xc8, 0x222A, // union
+                       0xc9, 0x2283, // propersuperset
+                       0xca, 0x2287, // reflexsuperset
+                       0xcb, 0x2284, // notsubset
+                       0xcc, 0x2282, // propersubset
+                       0xcd, 0x2286, // reflexsubset
+                       0xce, 0x2208, // element
+                       0xcf, 0x2209, // notelement
+                       0xd0, 0x2220, // angle
+                       0xd1, 0x2207, // gradient
+                       0xd2, 0xF6DA, // registerserif
+                       0xd3, 0xF6D9, // copyrightserif
+                       0xd4, 0xF6DB, // trademarkserif
+                       0xd5, 0x220F, // product
+                       0xd6, 0x221A, // radical
+                       0xd7, 0x22C5, // dotmath
+                       0xd8, 0x00AC, // logicalnot
+                       0xd9, 0x2227, // logicaland
+                       0xda, 0x2228, // logicalor
+                       0xdb, 0x21D4, // arrowdblboth
+                       0xdc, 0x21D0, // arrowdblleft
+                       0xdd, 0x21D1, // arrowdblup
+                       0xde, 0x21D2, // arrowdblright
+                       0xdf, 0x21D3, // arrowdbldown
+                       0xe0, 0x25CA, // lozenge
+                       0xe1, 0x2329, // angleleft
+                       0xe2, 0xF8E8, // registersans
+                       0xe3, 0xF8E9, // copyrightsans
+                       0xe4, 0xF8EA, // trademarksans
+                       0xe5, 0x2211, // summation
+                       0xe6, 0xF8EB, // parenlefttp
+                       0xe7, 0xF8EC, // parenleftex
+                       0xe8, 0xF8ED, // parenleftbt
+                       0xe9, 0xF8EE, // bracketlefttp
+                       0xea, 0xF8EF, // bracketleftex
+                       0xeb, 0xF8F0, // bracketleftbt
+                       0xec, 0xF8F1, // bracelefttp
+                       0xed, 0xF8F2, // braceleftmid
+                       0xee, 0xF8F3, // braceleftbt
+                       0xef, 0xF8F4, // braceex
+                       0xf1, 0x232A, // angleright
+                       0xf2, 0x222B, // integral
+                       0xf3, 0x2320, // integraltp
+                       0xf4, 0xF8F5, // integralex
+                       0xf5, 0x2321, // integralbt
+                       0xf6, 0xF8F6, // parenrighttp
+                       0xf7, 0xF8F7, // parenrightex
+                       0xf8, 0xF8F8, // parenrightbt
+                       0xf9, 0xF8F9, // bracketrighttp
+                       0xfa, 0xF8FA, // bracketrightex
+                       0xfb, 0xF8FB, // bracketrightbt
+                       0xfc, 0xF8FC, // bracerighttp
+                       0xfd, 0xF8FD, // bracerightmid
+                       0xfe, 0xF8FE, // bracerightbt
+       };
+
+       private static final int[] encZapfDingbatsEncoding
+                       = {
+                       0x20, 0x0020, // space
+                       0x20, 0x00A0, // space
+                       0x21, 0x2701, // a1
+                       0x22, 0x2702, // a2
+                       0x23, 0x2703, // a202
+                       0x24, 0x2704, // a3
+                       0x25, 0x260E, // a4
+                       0x26, 0x2706, // a5
+                       0x27, 0x2707, // a119
+                       0x28, 0x2708, // a118
+                       0x29, 0x2709, // a117
+                       0x2A, 0x261B, // a11
+                       0x2B, 0x261E, // a12
+                       0x2C, 0x270C, // a13
+                       0x2D, 0x270D, // a14
+                       0x2E, 0x270E, // a15
+                       0x2F, 0x270F, // a16
+                       0x30, 0x2710, // a105
+                       0x31, 0x2711, // a17
+                       0x32, 0x2712, // a18
+                       0x33, 0x2713, // a19
+                       0x34, 0x2714, // a20
+                       0x35, 0x2715, // a21
+                       0x36, 0x2716, // a22
+                       0x37, 0x2717, // a23
+                       0x38, 0x2718, // a24
+                       0x39, 0x2719, // a25
+                       0x3A, 0x271A, // a26
+                       0x3B, 0x271B, // a27
+                       0x3C, 0x271C, // a28
+                       0x3D, 0x271D, // a6
+                       0x3E, 0x271E, // a7
+                       0x3F, 0x271F, // a8
+                       0x40, 0x2720, // a9
+                       0x41, 0x2721, // a10
+                       0x42, 0x2722, // a29
+                       0x43, 0x2723, // a30
+                       0x44, 0x2724, // a31
+                       0x45, 0x2725, // a32
+                       0x46, 0x2726, // a33
+                       0x47, 0x2727, // a34
+                       0x48, 0x2605, // a35
+                       0x49, 0x2729, // a36
+                       0x4A, 0x272A, // a37
+                       0x4B, 0x272B, // a38
+                       0x4C, 0x272C, // a39
+                       0x4D, 0x272D, // a40
+                       0x4E, 0x272E, // a41
+                       0x4F, 0x272F, // a42
+                       0x50, 0x2730, // a43
+                       0x51, 0x2731, // a44
+                       0x52, 0x2732, // a45
+                       0x53, 0x2733, // a46
+                       0x54, 0x2734, // a47
+                       0x55, 0x2735, // a48
+                       0x56, 0x2736, // a49
+                       0x57, 0x2737, // a50
+                       0x58, 0x2738, // a51
+                       0x59, 0x2739, // a52
+                       0x5A, 0x273A, // a53
+                       0x5B, 0x273B, // a54
+                       0x5C, 0x273C, // a55
+                       0x5D, 0x273D, // a56
+                       0x5E, 0x273E, // a57
+                       0x5F, 0x273F, // a58
+                       0x60, 0x2740, // a59
+                       0x61, 0x2741, // a60
+                       0x62, 0x2742, // a61
+                       0x63, 0x2743, // a62
+                       0x64, 0x2744, // a63
+                       0x65, 0x2745, // a64
+                       0x66, 0x2746, // a65
+                       0x67, 0x2747, // a66
+                       0x68, 0x2748, // a67
+                       0x69, 0x2749, // a68
+                       0x6A, 0x274A, // a69
+                       0x6B, 0x274B, // a70
+                       0x6C, 0x25CF, // a71
+                       0x6D, 0x274D, // a72
+                       0x6E, 0x25A0, // a73
+                       0x6F, 0x274F, // a74
+                       0x70, 0x2750, // a203
+                       0x71, 0x2751, // a75
+                       0x72, 0x2752, // a204
+                       0x73, 0x25B2, // a76
+                       0x74, 0x25BC, // a77
+                       0x75, 0x25C6, // a78
+                       0x76, 0x2756, // a79
+                       0x77, 0x25D7, // a81
+                       0x78, 0x2758, // a82
+                       0x79, 0x2759, // a83
+                       0x7A, 0x275A, // a84
+                       0x7B, 0x275B, // a97
+                       0x7C, 0x275C, // a98
+                       0x7D, 0x275D, // a99
+                       0x7E, 0x275E, // a100
+                       0x80, 0xF8D7, // a89
+                       0x81, 0xF8D8, // a90
+                       0x82, 0xF8D9, // a93
+                       0x83, 0xF8DA, // a94
+                       0x84, 0xF8DB, // a91
+                       0x85, 0xF8DC, // a92
+                       0x86, 0xF8DD, // a205
+                       0x87, 0xF8DE, // a85
+                       0x88, 0xF8DF, // a206
+                       0x89, 0xF8E0, // a86
+                       0x8A, 0xF8E1, // a87
+                       0x8B, 0xF8E2, // a88
+                       0x8C, 0xF8E3, // a95
+                       0x8D, 0xF8E4, // a96
+                       0xA1, 0x2761, // a101
+                       0xA2, 0x2762, // a102
+                       0xA3, 0x2763, // a103
+                       0xA4, 0x2764, // a104
+                       0xA5, 0x2765, // a106
+                       0xA6, 0x2766, // a107
+                       0xA7, 0x2767, // a108
+                       0xA8, 0x2663, // a112
+                       0xA9, 0x2666, // a111
+                       0xAA, 0x2665, // a110
+                       0xAB, 0x2660, // a109
+                       0xAC, 0x2460, // a120
+                       0xAD, 0x2461, // a121
+                       0xAE, 0x2462, // a122
+                       0xAF, 0x2463, // a123
+                       0xB0, 0x2464, // a124
+                       0xB1, 0x2465, // a125
+                       0xB2, 0x2466, // a126
+                       0xB3, 0x2467, // a127
+                       0xB4, 0x2468, // a128
+                       0xB5, 0x2469, // a129
+                       0xB6, 0x2776, // a130
+                       0xB7, 0x2777, // a131
+                       0xB8, 0x2778, // a132
+                       0xB9, 0x2779, // a133
+                       0xBA, 0x277A, // a134
+                       0xBB, 0x277B, // a135
+                       0xBC, 0x277C, // a136
+                       0xBD, 0x277D, // a137
+                       0xBE, 0x277E, // a138
+                       0xBF, 0x277F, // a139
+                       0xC0, 0x2780, // a140
+                       0xC1, 0x2781, // a141
+                       0xC2, 0x2782, // a142
+                       0xC3, 0x2783, // a143
+                       0xC4, 0x2784, // a144
+                       0xC5, 0x2785, // a145
+                       0xC6, 0x2786, // a146
+                       0xC7, 0x2787, // a147
+                       0xC8, 0x2788, // a148
+                       0xC9, 0x2789, // a149
+                       0xCA, 0x278A, // a150
+                       0xCB, 0x278B, // a151
+                       0xCC, 0x278C, // a152
+                       0xCD, 0x278D, // a153
+                       0xCE, 0x278E, // a154
+                       0xCF, 0x278F, // a155
+                       0xD0, 0x2790, // a156
+                       0xD1, 0x2791, // a157
+                       0xD2, 0x2792, // a158
+                       0xD3, 0x2793, // a159
+                       0xD4, 0x2794, // a160
+                       0xD5, 0x2192, // a161
+                       0xD6, 0x2194, // a163
+                       0xD7, 0x2195, // a164
+                       0xD8, 0x2798, // a196
+                       0xD9, 0x2799, // a165
+                       0xDA, 0x279A, // a192
+                       0xDB, 0x279B, // a166
+                       0xDC, 0x279C, // a167
+                       0xDD, 0x279D, // a168
+                       0xDE, 0x279E, // a169
+                       0xDF, 0x279F, // a170
+                       0xE0, 0x27A0, // a171
+                       0xE1, 0x27A1, // a172
+                       0xE2, 0x27A2, // a173
+                       0xE3, 0x27A3, // a162
+                       0xE4, 0x27A4, // a174
+                       0xE5, 0x27A5, // a175
+                       0xE6, 0x27A6, // a176
+                       0xE7, 0x27A7, // a177
+                       0xE8, 0x27A8, // a178
+                       0xE9, 0x27A9, // a179
+                       0xEA, 0x27AA, // a193
+                       0xEB, 0x27AB, // a180
+                       0xEC, 0x27AC, // a199
+                       0xED, 0x27AD, // a181
+                       0xEE, 0x27AE, // a200
+                       0xEF, 0x27AF, // a182
+                       0xF1, 0x27B1, // a201
+                       0xF2, 0x27B2, // a183
+                       0xF3, 0x27B3, // a184
+                       0xF4, 0x27B4, // a197
+                       0xF5, 0x27B5, // a185
+                       0xF6, 0x27B6, // a194
+                       0xF7, 0x27B7, // a198
+                       0xF8, 0x27B8, // a186
+                       0xF9, 0x27B9, // a195
+                       0xFA, 0x27BA, // a187
+                       0xFB, 0x27BB, // a188
+                       0xFC, 0x27BC, // a189
+                       0xFD, 0x27BD, // a190
+                       0xFE, 0x27BE, // a191
+       };
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Font.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Font.java
new file mode 100644 (file)
index 0000000..3de98dc
--- /dev/null
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.Positionable;
+import com.jaredrummler.fontreader.complexscripts.fonts.Substitutable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class holds font state information and provides access to the font
+ * metrics.
+ */
+public class Font implements Substitutable, Positionable {
+
+       /**
+        * Extra Bold font weight
+        */
+       public static final int WEIGHT_EXTRA_BOLD = 800;
+
+       /**
+        * Bold font weight
+        */
+       public static final int WEIGHT_BOLD = 700;
+
+       /**
+        * Normal font weight
+        */
+       public static final int WEIGHT_NORMAL = 400;
+
+       /**
+        * Light font weight
+        */
+       public static final int WEIGHT_LIGHT = 200;
+
+       /**
+        * Normal font style
+        */
+       public static final String STYLE_NORMAL = "normal";
+
+       /**
+        * Italic font style
+        */
+       public static final String STYLE_ITALIC = "italic";
+
+       /**
+        * Oblique font style
+        */
+       public static final String STYLE_OBLIQUE = "oblique";
+
+       /**
+        * Inclined font style
+        */
+       public static final String STYLE_INCLINED = "inclined";
+
+       /**
+        * Default selection priority
+        */
+       public static final int PRIORITY_DEFAULT = 0;
+
+       /**
+        * Default fallback key
+        */
+       public static final FontTriplet DEFAULT_FONT = new FontTriplet("any", STYLE_NORMAL, WEIGHT_NORMAL, PRIORITY_DEFAULT);
+
+       private final String fontName;
+       private final FontTriplet triplet;
+       private final int fontSize;
+
+       /**
+        * normal or small-caps font
+        */
+       //private int fontVariant;
+
+       private final FontMetrics metric;
+
+       /**
+        * Main constructor
+        *
+        * @param key      key of the font
+        * @param triplet  the font triplet that was used to lookup this font (may be null)
+        * @param met      font metrics
+        * @param fontSize font size
+        */
+       public Font(String key, FontTriplet triplet, FontMetrics met, int fontSize) {
+               this.fontName = key;
+               this.triplet = triplet;
+               this.metric = met;
+               this.fontSize = fontSize;
+       }
+
+       /**
+        * Returns the associated font metrics object.
+        *
+        * @return the font metrics
+        */
+       public FontMetrics getFontMetrics() {
+               return this.metric;
+       }
+
+       /**
+        * Determines whether the font is a multibyte font.
+        *
+        * @return True if it is multibyte
+        */
+       public boolean isMultiByte() {
+               return getFontMetrics().isMultiByte();
+       }
+
+       /**
+        * Returns the font's ascender.
+        *
+        * @return the ascender
+        */
+       public int getAscender() {
+               return metric.getAscender(fontSize) / 1000;
+       }
+
+       /**
+        * Returns the font's CapHeight.
+        *
+        * @return the capital height
+        */
+       public int getCapHeight() {
+               return metric.getCapHeight(fontSize) / 1000;
+       }
+
+       /**
+        * Returns the font's Descender.
+        *
+        * @return the descender
+        */
+       public int getDescender() {
+               return metric.getDescender(fontSize) / 1000;
+       }
+
+       /**
+        * Returns the font's name.
+        *
+        * @return the font name
+        */
+       public String getFontName() {
+               return fontName;
+       }
+
+       /**
+        * @return the font triplet that selected this font
+        */
+       public FontTriplet getFontTriplet() {
+               return this.triplet;
+       }
+
+       /**
+        * Returns the font size
+        *
+        * @return the font size
+        */
+       public int getFontSize() {
+               return fontSize;
+       }
+
+       /**
+        * Returns the XHeight
+        *
+        * @return the XHeight
+        */
+       public int getXHeight() {
+               return metric.getXHeight(fontSize) / 1000;
+       }
+
+       /**
+        * @return true if the font has kerning info
+        */
+       public boolean hasKerning() {
+               return metric.hasKerningInfo();
+       }
+
+       /**
+        * @return true if the font has feature (i.e., at least one lookup matches)
+        */
+       public boolean hasFeature(int tableType, String script, String language, String feature) {
+               return metric.hasFeature(tableType, script, language, feature);
+       }
+
+       /**
+        * Returns the font's kerning table
+        *
+        * @return the kerning table
+        */
+       public Map<Integer, Map<Integer, Integer>> getKerning() {
+               if (metric.hasKerningInfo()) {
+                       return metric.getKerningInfo();
+               } else {
+                       return Collections.emptyMap();
+               }
+       }
+
+       /**
+        * Returns the amount of kerning between two characters.
+        * <p>
+        * The value returned measures in pt. So it is already adjusted for font size.
+        *
+        * @param ch1 first character
+        * @param ch2 second character
+        * @return the distance to adjust for kerning, 0 if there's no kerning
+        */
+       public int getKernValue(char ch1, char ch2) {
+               Map<Integer, Integer> kernPair = getKerning().get((int) ch1);
+               if (kernPair != null) {
+                       Integer width = kernPair.get((int) ch2);
+                       if (width != null) {
+                               return width.intValue() * getFontSize() / 1000;
+                       }
+               }
+               return 0;
+       }
+
+       /**
+        * Returns the amount of kerning between two characters.
+        * <p>
+        * The value returned measures in pt. So it is already adjusted for font size.
+        *
+        * @param ch1 first character
+        * @param ch2 second character
+        * @return the distance to adjust for kerning, 0 if there's no kerning
+        */
+       public int getKernValue(int ch1, int ch2) {
+               // TODO !BMP
+               if (ch1 > 0x10000) {
+                       return 0;
+               } else if ((ch1 >= 0xD800) && (ch1 <= 0xE000)) {
+                       return 0;
+               } else if (ch2 > 0x10000) {
+                       return 0;
+               } else if ((ch2 >= 0xD800) && (ch2 <= 0xE000)) {
+                       return 0;
+               } else {
+                       return getKernValue((char) ch1, (char) ch2);
+               }
+       }
+
+       /**
+        * Returns the width of a character
+        *
+        * @param charnum character to look up
+        * @return width of the character
+        */
+       public int getWidth(int charnum) {
+               // returns width of given character number in millipoints
+               return (metric.getWidth(charnum, fontSize) / 1000);
+       }
+
+       /**
+        * Map a java character (unicode) to a font character.
+        * Default uses CodePointMapping.
+        *
+        * @param c character to map
+        * @return the mapped character
+        */
+       public char mapChar(char c) {
+
+               if (metric instanceof Typeface) {
+                       return ((Typeface) metric).mapChar(c);
+               }
+
+               // Use default CodePointMapping
+               char d = CodePointMapping.getMapping("WinAnsiEncoding").mapChar(c);
+               if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) {
+                       c = d;
+               } else {
+                       c = Typeface.NOT_FOUND;
+               }
+
+               return c;
+       }
+
+       /**
+        * Determines whether this font contains a particular character/glyph.
+        *
+        * @param c character to check
+        * @return True if the character is supported, Falso otherwise
+        */
+       public boolean hasChar(char c) {
+               if (metric instanceof Typeface) {
+                       return ((Typeface) metric).hasChar(c);
+               } else {
+                       // Use default CodePointMapping
+                       return (CodePointMapping.getMapping("WinAnsiEncoding").mapChar(c) > 0);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               StringBuffer sbuf = new StringBuffer(super.toString());
+               sbuf.append('{');
+               sbuf.append(fontName);
+               sbuf.append(',');
+               sbuf.append(fontSize);
+               sbuf.append('}');
+               return sbuf.toString();
+       }
+
+       /**
+        * Helper method for getting the width of a unicode char
+        * from the current fontstate.
+        * This also performs some guessing on widths on various
+        * versions of space that might not exists in the font.
+        *
+        * @param c character to inspect
+        * @return the width of the character or -1 if no width available
+        */
+       public int getCharWidth(char c) {
+               int width;
+
+               if ((c == '\n') || (c == '\r') || (c == '\t') || (c == '\u00A0')) {
+                       width = getCharWidth(' ');
+               } else {
+                       if (hasChar(c)) {
+                               int mappedChar = mapChar(c);
+                               width = getWidth(mappedChar);
+                       } else {
+                               width = -1;
+                       }
+                       if (width <= 0) {
+                               // Estimate the width of spaces not represented in
+                               // the font
+                               int em = getFontSize(); //http://en.wikipedia.org/wiki/Em_(typography)
+                               int en = em / 2; //http://en.wikipedia.org/wiki/En_(typography)
+
+                               if (c == ' ') {
+                                       width = em;
+                               } else if (c == '\u2000') {
+                                       width = en;
+                               } else if (c == '\u2001') {
+                                       width = em;
+                               } else if (c == '\u2002') {
+                                       width = em / 2;
+                               } else if (c == '\u2003') {
+                                       width = getFontSize();
+                               } else if (c == '\u2004') {
+                                       width = em / 3;
+                               } else if (c == '\u2005') {
+                                       width = em / 4;
+                               } else if (c == '\u2006') {
+                                       width = em / 6;
+                               } else if (c == '\u2007') {
+                                       width = getCharWidth('0');
+                               } else if (c == '\u2008') {
+                                       width = getCharWidth('.');
+                               } else if (c == '\u2009') {
+                                       width = em / 5;
+                               } else if (c == '\u200A') {
+                                       width = em / 10;
+                               } else if (c == '\u200B') {
+                                       width = 0;
+                               } else if (c == '\u202F') {
+                                       width = getCharWidth(' ') / 2;
+                               } else if (c == '\u2060') {
+                                       width = 0;
+                               } else if (c == '\u3000') {
+                                       width = getCharWidth(' ') * 2;
+                               } else if (c == '\ufeff') {
+                                       width = 0;
+                               } else {
+                                       //Will be internally replaced by "#" if not found
+                                       width = getWidth(mapChar(c));
+                               }
+                       }
+               }
+
+               return width;
+       }
+
+       /**
+        * Helper method for getting the width of a unicode char
+        * from the current fontstate.
+        * This also performs some guessing on widths on various
+        * versions of space that might not exists in the font.
+        *
+        * @param c character to inspect
+        * @return the width of the character or -1 if no width available
+        */
+       public int getCharWidth(int c) {
+               if (c < 0x10000) {
+                       return getCharWidth((char) c);
+               } else {
+                       // TODO !BMP
+                       return -1;
+               }
+       }
+
+       /**
+        * Calculates the word width.
+        *
+        * @param word text to get width for
+        * @return the width of the text
+        */
+       public int getWordWidth(String word) {
+               if (word == null) {
+                       return 0;
+               }
+               int wordLength = word.length();
+               int width = 0;
+               char[] characters = new char[wordLength];
+               word.getChars(0, wordLength, characters, 0);
+               for (int i = 0; i < wordLength; i++) {
+                       width += getCharWidth(characters[i]);
+               }
+               return width;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean performsSubstitution() {
+               if (metric instanceof Substitutable) {
+                       Substitutable s = (Substitutable) metric;
+                       return s.performsSubstitution();
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public CharSequence performSubstitution(CharSequence cs,
+                                                                                       String script, String language, List associations, boolean retainControls) {
+               if (metric instanceof Substitutable) {
+                       Substitutable s = (Substitutable) metric;
+                       return s.performSubstitution(cs, script, language, associations, retainControls);
+               } else {
+                       throw new UnsupportedOperationException();
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public CharSequence reorderCombiningMarks(CharSequence cs, int[][] gpa,
+                                                                                         String script, String language, List associations) {
+               if (metric instanceof Substitutable) {
+                       Substitutable s = (Substitutable) metric;
+                       return s.reorderCombiningMarks(cs, gpa, script, language, associations);
+               } else {
+                       throw new UnsupportedOperationException();
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean performsPositioning() {
+               if (metric instanceof Positionable) {
+                       Positionable p = (Positionable) metric;
+                       return p.performsPositioning();
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int[][] performPositioning(CharSequence cs, String script, String language, int fontSize) {
+               if (metric instanceof Positionable) {
+                       Positionable p = (Positionable) metric;
+                       return p.performPositioning(cs, script, language, fontSize);
+               } else {
+                       throw new UnsupportedOperationException();
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int[][] performPositioning(CharSequence cs, String script, String language) {
+               return performPositioning(cs, script, language, fontSize);
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontMetrics.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontMetrics.java
new file mode 100644 (file)
index 0000000..4aeaf69
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+import java.awt.*;
+import java.net.URI;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Main interface for access to font metrics.
+ */
+public interface FontMetrics {
+
+       /**
+        * Returns the URI of the font file from which these metrics were loaded.
+        *
+        * @return the font file's URI
+        */
+       URI getFontURI();
+
+       /**
+        * Returns the "PostScript" font name (Example: "Helvetica-BoldOblique").
+        *
+        * @return the font name
+        */
+       String getFontName();
+
+       /**
+        * Returns the font's full name (Example: "Helvetica Bold Oblique").
+        *
+        * @return the font's full name
+        */
+       String getFullName();
+
+       /**
+        * Returns the font's family names as a Set of Strings (Example: "Helvetica").
+        *
+        * @return the font's family names (a Set of Strings)
+        */
+       Set<String> getFamilyNames();
+
+       /**
+        * Returns the font name for font embedding (may include a prefix, Example: "1E28bcArialMT").
+        *
+        * @return the name for font embedding
+        */
+       String getEmbedFontName();
+
+       /**
+        * Returns the type of the font.
+        *
+        * @return the font type
+        */
+       FontType getFontType();
+
+       /**
+        * Returns the maximum ascent of the font described by this
+        * FontMetrics object. Note: This is not the same as getAscender().
+        *
+        * @param size font size
+        * @return ascent in milliponts
+        */
+       int getMaxAscent(int size);
+
+       /**
+        * Returns the ascent of the font described by this
+        * FontMetrics object. It returns the nominal ascent within the em box.
+        *
+        * @param size font size
+        * @return ascent in milliponts
+        */
+       int getAscender(int size);
+
+       /**
+        * Returns the size of a capital letter measured from the font's baseline.
+        *
+        * @param size font size
+        * @return height of capital characters
+        */
+       int getCapHeight(int size);
+
+       /**
+        * Returns the descent of the font described by this
+        * FontMetrics object.
+        *
+        * @param size font size
+        * @return descent in milliponts
+        */
+       int getDescender(int size);
+
+       /**
+        * Determines the typical font height of this
+        * FontMetrics object
+        *
+        * @param size font size
+        * @return font height in millipoints
+        */
+       int getXHeight(int size);
+
+       /**
+        * Return the width (in 1/1000ths of point size) of the character at
+        * code point i.
+        *
+        * @param i    code point index
+        * @param size font size
+        * @return the width of the character
+        */
+       int getWidth(int i, int size);
+
+       /**
+        * Return the array of widths.
+        * <p>
+        * This is used to get an array for inserting in an output format.
+        * It should not be used for lookup.
+        *
+        * @return an array of widths
+        */
+       int[] getWidths();
+
+       /**
+        * Returns the bounding box of the glyph at the given index, for the given font size.
+        *
+        * @param glyphIndex glyph index
+        * @param size       font size
+        * @return the scaled bounding box scaled in 1/1000ths of the given size
+        */
+       Rectangle getBoundingBox(int glyphIndex, int size);
+
+       /**
+        * Indicates if the font has kerning information.
+        *
+        * @return true if kerning is available.
+        */
+       boolean hasKerningInfo();
+
+       /**
+        * Returns the kerning map for the font.
+        *
+        * @return the kerning map
+        */
+       Map<Integer, Map<Integer, Integer>> getKerningInfo();
+
+       /**
+        * Returns the distance from the baseline to the center of the underline (negative
+        * value indicates below baseline).
+        *
+        * @param size font size
+        * @return the position in 1/1000ths of the font size
+        */
+       int getUnderlinePosition(int size);
+
+       /**
+        * Returns the thickness of the underline.
+        *
+        * @param size font size
+        * @return the thickness in 1/1000ths of the font size
+        */
+       int getUnderlineThickness(int size);
+
+       /**
+        * Returns the distance from the baseline to the center of the strikeout line
+        * (negative value indicates below baseline).
+        *
+        * @param size font size
+        * @return the position in 1/1000ths of the font size
+        */
+       int getStrikeoutPosition(int size);
+
+       /**
+        * Returns the thickness of the strikeout line.
+        *
+        * @param size font size
+        * @return the thickness in 1/1000ths of the font size
+        */
+       int getStrikeoutThickness(int size);
+
+       /**
+        * Determine if metrics supports specific feature in specified font table.
+        *
+        * @param tableType type of table (GSUB, GPOS, ...), see GlyphTable.GLYPH_TABLE_TYPE_*
+        * @param script    to qualify feature lookup
+        * @param language  to qualify feature lookup
+        * @param feature   to test
+        * @return true if feature supported (and has at least one lookup)
+        */
+       boolean hasFeature(int tableType, String script, String language, String feature);
+
+       /**
+        * Determines whether the font is a multibyte font.
+        *
+        * @return True if it is multibyte
+        */
+       boolean isMultiByte();
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontTriplet.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontTriplet.java
new file mode 100644 (file)
index 0000000..0942143
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+
+/**
+ * FontTriplet contains information on name, style and weight of one font
+ */
+public class FontTriplet implements Comparable<FontTriplet>, Serializable {
+
+       public static final FontTriplet DEFAULT_FONT_TRIPLET
+                       = new FontTriplet("any", Font.STYLE_NORMAL, Font.WEIGHT_NORMAL);
+
+       /**
+        * serial version UID
+        */
+       private static final long serialVersionUID = 1168991106658033508L;
+
+       private String name;
+       private String style;
+       private int weight;
+       private int priority; // priority of this triplet/font mapping
+
+       //This is only a cache
+       private transient String key;
+
+       public FontTriplet() {
+               this(null, null, 0);
+       }
+
+       /**
+        * Creates a new font triplet.
+        *
+        * @param name   font name
+        * @param style  font style (normal, italic etc.)
+        * @param weight font weight (100, 200, 300...800, 900)
+        */
+       public FontTriplet(String name, String style, int weight) {
+               this(name, style, weight, Font.PRIORITY_DEFAULT);
+       }
+
+       /**
+        * Creates a new font triplet.
+        *
+        * @param name     font name
+        * @param style    font style (normal, italic etc.)
+        * @param weight   font weight (100, 200, 300...800, 900)
+        * @param priority priority of this triplet/font mapping
+        */
+       public FontTriplet(String name, String style, int weight, int priority) {
+               this.name = name;
+               this.style = style;
+               this.weight = weight;
+               this.priority = priority;
+       }
+
+       private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
+               ois.defaultReadObject();
+       }
+
+       /**
+        * @return the font name
+        */
+       public String getName() {
+               return name;
+       }
+
+       /**
+        * @return the font style
+        */
+       public String getStyle() {
+               return style;
+       }
+
+       /**
+        * @return the font weight
+        */
+       public int getWeight() {
+               return weight;
+       }
+
+       /**
+        * @return the priority of this triplet/font mapping
+        */
+       public int getPriority() {
+               return priority;
+       }
+
+       private String getKey() {
+               if (this.key == null) {
+                       //This caches the combined key
+                       this.key = getName() + "," + getStyle() + "," + getWeight();
+               }
+               return this.key;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int compareTo(FontTriplet o) {
+               return getKey().compareTo(o.getKey());
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int hashCode() {
+               return toString().hashCode();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean equals(Object obj) {
+               if (obj == null) {
+                       return false;
+               } else if (obj == this) {
+                       return true;
+               } else {
+                       if (obj instanceof FontTriplet) {
+                               FontTriplet other = (FontTriplet) obj;
+                               return (getName().equals(other.getName())
+                                               && getStyle().equals(other.getStyle())
+                                               && (getWeight() == other.getWeight()));
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String toString() {
+               return getKey();
+       }
+
+       /**
+        * Matcher interface for {@link FontTriplet}.
+        */
+       public interface Matcher {
+
+               /**
+                * Indicates whether the given {@link FontTriplet} matches a particular criterium.
+                *
+                * @param triplet the font triplet
+                * @return true if the font triplet is a match
+                */
+               boolean matches(FontTriplet triplet);
+       }
+}
+
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontType.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontType.java
new file mode 100644 (file)
index 0000000..7944377
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+/**
+ * This class enumerates all supported font types.
+ */
+public class FontType {
+
+       /**
+        * Collective identifier for "other" font types
+        */
+       public static final FontType OTHER = new FontType("Other", 0);
+       /**
+        * Adobe Type 0 fonts (composite font)
+        */
+       public static final FontType TYPE0 = new FontType("Type0", 1);
+       /**
+        * Adobe Type 1 fonts
+        */
+       public static final FontType TYPE1 = new FontType("Type1", 2);
+       /**
+        * Adobe Multiple Master Type 1 fonts
+        */
+       public static final FontType MMTYPE1 = new FontType("MMType1", 3);
+       /**
+        * Adobe Type 3 fonts ("user-defined" fonts)
+        */
+       public static final FontType TYPE3 = new FontType("Type3", 4);
+       /**
+        * TrueType fonts
+        */
+       public static final FontType TRUETYPE = new FontType("TrueType", 5);
+
+       public static final FontType TYPE1C = new FontType("Type1C", 6);
+
+       public static final FontType CIDTYPE0 = new FontType("CIDFontType0", 7);
+
+       private final String name;
+       private final int value;
+
+       /**
+        * Construct a font type.
+        *
+        * @param name  a font type name
+        * @param value a font type value
+        */
+       protected FontType(String name, int value) {
+               this.name = name;
+               this.value = value;
+       }
+
+       /**
+        * Returns the FontType by name.
+        *
+        * @param name Name of the font type to look up
+        * @return the font type
+        */
+       public static FontType byName(String name) {
+               if (name.equalsIgnoreCase(FontType.OTHER.getName())) {
+                       return FontType.OTHER;
+               } else if (name.equalsIgnoreCase(FontType.TYPE0.getName())) {
+                       return FontType.TYPE0;
+               } else if (name.equalsIgnoreCase(FontType.TYPE1.getName())) {
+                       return FontType.TYPE1;
+               } else if (name.equalsIgnoreCase(FontType.MMTYPE1.getName())) {
+                       return FontType.MMTYPE1;
+               } else if (name.equalsIgnoreCase(FontType.TYPE3.getName())) {
+                       return FontType.TYPE3;
+               } else if (name.equalsIgnoreCase(FontType.TRUETYPE.getName())) {
+                       return FontType.TRUETYPE;
+               } else {
+                       throw new IllegalArgumentException("Invalid font type: " + name);
+               }
+       }
+
+       /**
+        * Returns the FontType by value.
+        *
+        * @param value Value of the font type to look up
+        * @return the font type
+        */
+       public static FontType byValue(int value) {
+               if (value == FontType.OTHER.getValue()) {
+                       return FontType.OTHER;
+               } else if (value == FontType.TYPE0.getValue()) {
+                       return FontType.TYPE0;
+               } else if (value == FontType.TYPE1.getValue()) {
+                       return FontType.TYPE1;
+               } else if (value == FontType.MMTYPE1.getValue()) {
+                       return FontType.MMTYPE1;
+               } else if (value == FontType.TYPE3.getValue()) {
+                       return FontType.TYPE3;
+               } else if (value == FontType.TRUETYPE.getValue()) {
+                       return FontType.TRUETYPE;
+               } else {
+                       throw new IllegalArgumentException("Invalid font type: " + value);
+               }
+       }
+
+       /**
+        * Returns the name
+        *
+        * @return the name
+        */
+       public String getName() {
+               return name;
+       }
+
+       /**
+        * Returns the value
+        *
+        * @return the value
+        */
+       public int getValue() {
+               return value;
+       }
+
+       @Override
+       public String toString() {
+               return name;
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontUtil.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontUtil.java
new file mode 100644 (file)
index 0000000..8970ade
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+/**
+ * Font utilities.
+ */
+public final class FontUtil {
+
+       private FontUtil() {
+       }
+
+       /**
+        * Parses an CSS2 (SVG and XSL-FO) font weight (normal, bold, 100-900) to
+        * an integer.
+        * See http://www.w3.org/TR/REC-CSS2/fonts.html#propdef-font-weight
+        * TODO: Implement "lighter" and "bolder".
+        *
+        * @param text the font weight to parse
+        * @return an integer between 100 and 900 (100, 200, 300...)
+        */
+       public static int parseCSS2FontWeight(String text) {
+               int weight = 400;
+               try {
+                       weight = Integer.parseInt(text);
+                       weight = (weight / 100) * 100;
+                       weight = Math.max(weight, 100);
+                       weight = Math.min(weight, 900);
+               } catch (NumberFormatException nfe) {
+                       //weight is no number, so convert symbolic name to number
+                       if (text.equals("normal")) {
+                               weight = 400;
+                       } else if (text.equals("bold")) {
+                               weight = 700;
+                       } else {
+                               throw new IllegalArgumentException(
+                                               "Illegal value for font weight: '"
+                                                               + text
+                                                               + "'. Use one of: 100, 200, 300, "
+                                                               + "400, 500, 600, 700, 800, 900, "
+                                                               + "normal (=400), bold (=700)");
+                       }
+               }
+               return weight;
+       }
+
+       /**
+        * Removes all white space from a string (used primarily for font names)
+        *
+        * @param str the string
+        * @return the processed result
+        */
+       public static String stripWhiteSpace(String str) {
+               if (str != null) {
+                       StringBuffer stringBuffer = new StringBuffer(str.length());
+                       for (int i = 0, strLen = str.length(); i < strLen; i++) {
+                               final char ch = str.charAt(i);
+                               if (ch != ' ' && ch != '\r' && ch != '\n' && ch != '\t') {
+                                       stringBuffer.append(ch);
+                               }
+                       }
+                       return stringBuffer.toString();
+               }
+               return str;
+       }
+
+       /**
+        * font constituent names which identify a font as being of "italic" style
+        */
+       private static final String[] ITALIC_WORDS = {
+                       Font.STYLE_ITALIC, Font.STYLE_OBLIQUE, Font.STYLE_INCLINED
+       };
+
+       /**
+        * font constituent names which identify a font as being of "light" weight
+        */
+       private static final String[] LIGHT_WORDS = {"light"};
+       /**
+        * font constituent names which identify a font as being of "medium" weight
+        */
+       private static final String[] MEDIUM_WORDS = {"medium"};
+       /**
+        * font constituent names which identify a font as being of "demi/semi" weight
+        */
+       private static final String[] DEMI_WORDS = {"demi", "semi"};
+       /**
+        * font constituent names which identify a font as being of "bold" weight
+        */
+       private static final String[] BOLD_WORDS = {"bold"};
+       /**
+        * font constituent names which identify a font as being of "extra bold" weight
+        */
+       private static final String[] EXTRA_BOLD_WORDS = {"extrabold", "extra bold", "black",
+                       "heavy", "ultra", "super"};
+
+       /**
+        * Guesses the font style of a font using its name.
+        *
+        * @param fontName the font name
+        * @return "normal" or "italic"
+        */
+       public static String guessStyle(String fontName) {
+               if (fontName != null) {
+                       for (int i = 0; i < ITALIC_WORDS.length; i++) {
+                               if (fontName.indexOf(ITALIC_WORDS[i]) != -1) {
+                                       return Font.STYLE_ITALIC;
+                               }
+                       }
+               }
+               return Font.STYLE_NORMAL;
+       }
+
+       /**
+        * Guesses the font weight of a font using its name.
+        *
+        * @param fontName the font name
+        * @return an integer between 100 and 900
+        */
+       public static int guessWeight(String fontName) {
+               // weight
+               int weight = Font.WEIGHT_NORMAL;
+
+               for (int i = 0; i < BOLD_WORDS.length; i++) {
+                       if (fontName.indexOf(BOLD_WORDS[i]) != -1) {
+                               weight = Font.WEIGHT_BOLD;
+                               break;
+                       }
+               }
+               for (int i = 0; i < MEDIUM_WORDS.length; i++) {
+                       if (fontName.indexOf(MEDIUM_WORDS[i]) != -1) {
+                               weight = Font.WEIGHT_NORMAL + 100; //500
+                               break;
+                       }
+               }
+               //Search for "semi/demi" before "light", but after "bold"
+               //(normally semi/demi-bold is meant, but it can also be semi/demi-light)
+               for (int i = 0; i < DEMI_WORDS.length; i++) {
+                       if (fontName.indexOf(DEMI_WORDS[i]) != -1) {
+                               weight = Font.WEIGHT_BOLD - 100; //600
+                               break;
+                       }
+               }
+               for (int i = 0; i < EXTRA_BOLD_WORDS.length; i++) {
+                       if (fontName.indexOf(EXTRA_BOLD_WORDS[i]) != -1) {
+                               weight = Font.WEIGHT_EXTRA_BOLD;
+                               break;
+                       }
+               }
+               for (int i = 0; i < LIGHT_WORDS.length; i++) {
+                       if (fontName.indexOf(LIGHT_WORDS[i]) != -1) {
+                               weight = Font.WEIGHT_LIGHT;
+                               break;
+                       }
+               }
+               return weight;
+       }
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitution.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitution.java
new file mode 100644 (file)
index 0000000..3b8ea0c
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+// CSOFF: LineLengthCheck
+
+/**
+ * <p>The <code>GlyphSubstitution</code> interface is implemented by a glyph substitution subtable
+ * that supports the determination of glyph substitution information based on script and
+ * language of the corresponding character content.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface GlyphSubstitution {
+
+       /**
+        * Perform glyph substitution at the current index, mutating the substitution state object as required.
+        * Only the context associated with the current index is processed.
+        *
+        * @param ss glyph substitution state object
+        * @return true if the glyph subtable was applied, meaning that the current context matches the
+        * associated input context glyph coverage table
+        */
+       boolean substitute(GlyphSubstitutionState ss);
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionState.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionState.java
new file mode 100644 (file)
index 0000000..aa92b59
--- /dev/null
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphProcessingState;
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.ScriptContextTester;
+
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <p>The <code>GlyphSubstitutionState</code> implements an state object used during glyph substitution
+ * processing.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GlyphSubstitutionState extends GlyphProcessingState {
+
+       /**
+        * alternates index
+        */
+       private int[] alternatesIndex;
+       /**
+        * current output glyph sequence
+        */
+       private IntBuffer ogb;
+       /**
+        * current output glyph to character associations
+        */
+       private List oal;
+       /**
+        * character association predications
+        */
+       private boolean predications;
+
+       /**
+        * Construct default (reset) glyph substitution state.
+        */
+       public GlyphSubstitutionState() {
+       }
+
+       /**
+        * Construct glyph substitution state.
+        *
+        * @param gs       input glyph sequence
+        * @param script   script identifier
+        * @param language language identifier
+        * @param feature  feature identifier
+        * @param sct      script context tester (or null)
+        */
+       public GlyphSubstitutionState(GlyphSequence gs, String script, String language, String feature,
+                                                                 ScriptContextTester sct) {
+               super(gs, script, language, feature, sct);
+               this.ogb = IntBuffer.allocate(gs.getGlyphCount());
+               this.oal = new ArrayList(gs.getGlyphCount());
+               this.predications = gs.getPredications();
+       }
+
+       /**
+        * Construct glyph substitution state using an existing state object using shallow copy
+        * except as follows: input glyph sequence is copied deep except for its characters array.
+        *
+        * @param ss existing positioning state to copy from
+        */
+       public GlyphSubstitutionState(GlyphSubstitutionState ss) {
+               super(ss);
+               this.ogb = IntBuffer.allocate(indexLast);
+               this.oal = new ArrayList(indexLast);
+       }
+
+       /**
+        * Reset glyph substitution state.
+        *
+        * @param gs       input glyph sequence
+        * @param script   script identifier
+        * @param language language identifier
+        * @param feature  feature identifier
+        * @param sct      script context tester (or null)
+        */
+       public GlyphSubstitutionState reset(GlyphSequence gs, String script, String language, String feature,
+                                                                               ScriptContextTester sct) {
+               super.reset(gs, script, language, feature, sct);
+               this.alternatesIndex = null;
+               this.ogb = IntBuffer.allocate(gs.getGlyphCount());
+               this.oal = new ArrayList(gs.getGlyphCount());
+               this.predications = gs.getPredications();
+               return this;
+       }
+
+       /**
+        * Set alternates indices.
+        *
+        * @param alternates array of alternates indices ordered by coverage index
+        */
+       public void setAlternates(int[] alternates) {
+               this.alternatesIndex = alternates;
+       }
+
+       /**
+        * Obtain alternates index associated with specified coverage index. An alternates
+        * index is used to select among stylistic alternates of a glyph at a particular
+        * coverage index. This information must be provided by the document itself (in the
+        * form of an extension attribute value), since a font has no way to determine which
+        * alternate the user desires.
+        *
+        * @param ci coverage index
+        * @return an alternates index
+        */
+       public int getAlternatesIndex(int ci) {
+               if (alternatesIndex == null) {
+                       return 0;
+               } else if ((ci < 0) || (ci > alternatesIndex.length)) {
+                       return 0;
+               } else {
+                       return alternatesIndex[ci];
+               }
+       }
+
+       /**
+        * Put (write) glyph into glyph output buffer.
+        *
+        * @param glyph       to write
+        * @param a           character association that applies to glyph
+        * @param predication a predication value to add to association A if predications enabled
+        */
+       public void putGlyph(int glyph, CharAssociation a, Object predication) {
+               if (!ogb.hasRemaining()) {
+                       ogb = growBuffer(ogb);
+               }
+               ogb.put(glyph);
+               if (predications && (predication != null)) {
+                       a.setPredication(feature, predication);
+               }
+               oal.add(a);
+       }
+
+       /**
+        * Put (write) array of glyphs into glyph output buffer.
+        *
+        * @param glyphs       to write
+        * @param associations array of character associations that apply to glyphs
+        * @param predication  optional predicaion object to be associated with glyphs' associations
+        */
+       public void putGlyphs(int[] glyphs, CharAssociation[] associations, Object predication) {
+               assert glyphs != null;
+               assert associations != null;
+               assert associations.length >= glyphs.length;
+               for (int i = 0, n = glyphs.length; i < n; i++) {
+                       putGlyph(glyphs[i], associations[i], predication);
+               }
+       }
+
+       /**
+        * Obtain output glyph sequence.
+        *
+        * @return newly constructed glyph sequence comprised of original
+        * characters, output glyphs, and output associations
+        */
+       public GlyphSequence getOutput() {
+               int position = ogb.position();
+               if (position > 0) {
+                       ogb.limit(position);
+                       ogb.rewind();
+                       return new GlyphSequence(igs.getCharacters(), ogb, oal);
+               } else {
+                       return igs;
+               }
+       }
+
+       /**
+        * Apply substitution subtable to current state at current position (only),
+        * resulting in the consumption of zero or more input glyphs, and possibly
+        * replacing the current input glyphs starting at the current position, in
+        * which case it is possible that indexLast is altered to be either less than
+        * or greater than its value prior to this application.
+        *
+        * @param st the glyph substitution subtable to apply
+        * @return true if subtable applied, or false if it did not (e.g., its
+        * input coverage table did not match current input context)
+        */
+       public boolean apply(GlyphSubstitutionSubtable st) {
+               assert st != null;
+               updateSubtableState(st);
+               boolean applied = st.substitute(this);
+               return applied;
+       }
+
+       /**
+        * Apply a sequence of matched rule lookups to the <code>nig</code> input glyphs
+        * starting at the current position. If lookups are non-null and non-empty, then
+        * all input glyphs specified by <code>nig</code> are consumed irregardless of
+        * whether any specified lookup applied.
+        *
+        * @param lookups array of matched lookups (or null)
+        * @param nig     number of glyphs in input sequence, starting at current position, to which
+        *                the lookups are to apply, and to be consumed once the application has finished
+        * @return true if lookups are non-null and non-empty; otherwise, false
+        */
+       public boolean apply(GlyphTable.RuleLookup[] lookups, int nig) {
+               // int nbg = index;
+               int nlg = indexLast - (index + nig);
+               int nog = 0;
+               if ((lookups != null) && (lookups.length > 0)) {
+                       // apply each rule lookup to extracted input glyph array
+                       for (int i = 0, n = lookups.length; i < n; i++) {
+                               GlyphTable.RuleLookup l = lookups[i];
+                               if (l != null) {
+                                       GlyphTable.LookupTable lt = l.getLookup();
+                                       if (lt != null) {
+                                               // perform substitution on a copy of previous state
+                                               GlyphSubstitutionState ss = new GlyphSubstitutionState(this);
+                                               // apply lookup table substitutions
+                                               GlyphSequence gs = lt.substitute(ss, l.getSequenceIndex());
+                                               // replace current input sequence starting at current position with result
+                                               if (replaceInput(0, -1, gs)) {
+                                                       nog = gs.getGlyphCount() - nlg;
+                                               }
+                                       }
+                               }
+                       }
+                       // output glyphs and associations
+                       putGlyphs(getGlyphs(0, nog, false, null, null, null), getAssociations(0, nog, false, null, null, null), null);
+                       // consume replaced input glyphs
+                       consume(nog);
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Apply default application semantices; namely, consume one input glyph,
+        * writing that glyph (and its association) to the output glyphs (and associations).
+        */
+       public void applyDefault() {
+               super.applyDefault();
+               int gi = getGlyph();
+               if (gi != 65535) {
+                       putGlyph(gi, getAssociation(), null);
+               }
+       }
+
+       private static IntBuffer growBuffer(IntBuffer ib) {
+               int capacity = ib.capacity();
+               int capacityNew = capacity * 2;
+               IntBuffer ibNew = IntBuffer.allocate(capacityNew);
+               ib.rewind();
+               return ibNew.put(ib);
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionSubtable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionSubtable.java
new file mode 100644 (file)
index 0000000..2d07b83
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+// CSOFF: LineLengthCheck
+
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphCoverageTable;
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.ScriptContextTester;
+
+/**
+ * <p>The <code>GlyphSubstitutionSubtable</code> implements an abstract base of a glyph substitution subtable,
+ * providing a default implementation of the <code>GlyphSubstitution</code> interface.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public abstract class GlyphSubstitutionSubtable extends GlyphSubtable implements GlyphSubstitution {
+
+       private static final GlyphSubstitutionState STATE = new GlyphSubstitutionState();
+
+       /**
+        * Instantiate a <code>GlyphSubstitutionSubtable</code>.
+        *
+        * @param id       subtable identifier
+        * @param sequence subtable sequence
+        * @param flags    subtable flags
+        * @param format   subtable format
+        * @param coverage subtable coverage table
+        */
+       protected GlyphSubstitutionSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage) {
+               super(id, sequence, flags, format, coverage);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int getTableType() {
+               return GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String getTypeName() {
+               return GlyphSubstitutionTable.getLookupTypeName(getType());
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean isCompatible(GlyphSubtable subtable) {
+               return subtable instanceof GlyphSubstitutionSubtable;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean usesReverseScan() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean substitute(GlyphSubstitutionState ss) {
+               return false;
+       }
+
+       /**
+        * Apply substitutions using specified state and subtable array. For each position in input sequence,
+        * apply subtables in order until some subtable applies or none remain. If no subtable applied or no
+        * input was consumed for a given position, then apply default action (copy input glyph and advance).
+        * If <code>sequenceIndex</code> is non-negative, then apply subtables only when current position
+        * matches <code>sequenceIndex</code> in relation to the starting position. Furthermore, upon
+        * successful application at <code>sequenceIndex</code>, then apply default action for all remaining
+        * glyphs in input sequence.
+        *
+        * @param ss            substitution state
+        * @param sta           array of subtables to apply
+        * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
+        * @return output glyph sequence
+        */
+       public static final GlyphSequence substitute(GlyphSubstitutionState ss, GlyphSubstitutionSubtable[] sta,
+                                                                                                int sequenceIndex) {
+               int sequenceStart = ss.getPosition();
+               boolean appliedOneShot = false;
+               while (ss.hasNext()) {
+                       boolean applied = false;
+                       if (!appliedOneShot && ss.maybeApplicable()) {
+                               for (int i = 0, n = sta.length; !applied && (i < n); i++) {
+                                       if (sequenceIndex < 0) {
+                                               applied = ss.apply(sta[i]);
+                                       } else if (ss.getPosition() == (sequenceStart + sequenceIndex)) {
+                                               applied = ss.apply(sta[i]);
+                                               if (applied) {
+                                                       appliedOneShot = true;
+                                               }
+                                       }
+                               }
+                       }
+                       if (!applied || !ss.didConsume()) {
+                               ss.applyDefault();
+                       }
+                       ss.next();
+               }
+               return ss.getOutput();
+       }
+
+       /**
+        * Apply substitutions.
+        *
+        * @param gs       input glyph sequence
+        * @param script   tag
+        * @param language tag
+        * @param feature  tag
+        * @param sta      subtable array
+        * @param sct      script context tester
+        * @return output glyph sequence
+        */
+       public static final GlyphSequence substitute(GlyphSequence gs, String script, String language, String feature,
+                                                                                                GlyphSubstitutionSubtable[] sta, ScriptContextTester sct) {
+               synchronized (STATE) {
+                       return substitute(STATE.reset(gs, script, language, feature, sct), sta, -1);
+               }
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionTable.java
new file mode 100644 (file)
index 0000000..1df75af
--- /dev/null
@@ -0,0 +1,1790 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.AdvancedTypographicTableFormatException;
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphClassTable;
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphCoverageTable;
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphDefinitionTable;
+import com.jaredrummler.fontreader.complexscripts.scripts.ScriptProcessor;
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.GlyphTester;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>The <code>GlyphSubstitutionTable</code> class is a glyph table that implements
+ * <code>GlyphSubstitution</code> functionality.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GlyphSubstitutionTable extends GlyphTable {
+
+       /**
+        * single substitution subtable type
+        */
+       public static final int GSUB_LOOKUP_TYPE_SINGLE = 1;
+       /**
+        * multiple substitution subtable type
+        */
+       public static final int GSUB_LOOKUP_TYPE_MULTIPLE = 2;
+       /**
+        * alternate substitution subtable type
+        */
+       public static final int GSUB_LOOKUP_TYPE_ALTERNATE = 3;
+       /**
+        * ligature substitution subtable type
+        */
+       public static final int GSUB_LOOKUP_TYPE_LIGATURE = 4;
+       /**
+        * contextual substitution subtable type
+        */
+       public static final int GSUB_LOOKUP_TYPE_CONTEXTUAL = 5;
+       /**
+        * chained contextual substitution subtable type
+        */
+       public static final int GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL = 6;
+       /**
+        * extension substitution substitution subtable type
+        */
+       public static final int GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION = 7;
+       /**
+        * reverse chained contextual single substitution subtable type
+        */
+       public static final int GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE = 8;
+
+       /**
+        * Instantiate a <code>GlyphSubstitutionTable</code> object using the specified lookups
+        * and subtables.
+        *
+        * @param gdef      glyph definition table that applies
+        * @param lookups   a map of lookup specifications to subtable identifier strings
+        * @param subtables a list of identified subtables
+        */
+       public GlyphSubstitutionTable(GlyphDefinitionTable gdef, Map lookups, List subtables) {
+               super(gdef, lookups);
+               if ((subtables == null) || (subtables.size() == 0)) {
+                       throw new AdvancedTypographicTableFormatException("subtables must be non-empty");
+               } else {
+                       for (Iterator it = subtables.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (o instanceof GlyphSubstitutionSubtable) {
+                                       addSubtable((GlyphSubtable) o);
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException("subtable must be a glyph substitution subtable");
+                               }
+                       }
+                       freezeSubtables();
+               }
+       }
+
+       /**
+        * Perform substitution processing using all matching lookups.
+        *
+        * @param gs       an input glyph sequence
+        * @param script   a script identifier
+        * @param language a language identifier
+        * @return the substituted (output) glyph sequence
+        */
+       public GlyphSequence substitute(GlyphSequence gs, String script, String language) {
+               GlyphSequence ogs;
+               Map/*<LookupSpec,List<LookupTable>>*/ lookups = matchLookups(script, language, "*");
+               if ((lookups != null) && (lookups.size() > 0)) {
+                       ScriptProcessor sp = ScriptProcessor.getInstance(script);
+                       ogs = sp.substitute(this, gs, script, language, lookups);
+               } else {
+                       ogs = gs;
+               }
+               return ogs;
+       }
+
+       /**
+        * Map a lookup type name to its constant (integer) value.
+        *
+        * @param name lookup type name
+        * @return lookup type
+        */
+       public static int getLookupTypeFromName(String name) {
+               int t;
+               String s = name.toLowerCase();
+               if ("single".equals(s)) {
+                       t = GSUB_LOOKUP_TYPE_SINGLE;
+               } else if ("multiple".equals(s)) {
+                       t = GSUB_LOOKUP_TYPE_MULTIPLE;
+               } else if ("alternate".equals(s)) {
+                       t = GSUB_LOOKUP_TYPE_ALTERNATE;
+               } else if ("ligature".equals(s)) {
+                       t = GSUB_LOOKUP_TYPE_LIGATURE;
+               } else if ("contextual".equals(s)) {
+                       t = GSUB_LOOKUP_TYPE_CONTEXTUAL;
+               } else if ("chainedcontextual".equals(s)) {
+                       t = GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
+               } else if ("extensionsubstitution".equals(s)) {
+                       t = GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION;
+               } else if ("reversechainiingcontextualsingle".equals(s)) {
+                       t = GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE;
+               } else {
+                       t = -1;
+               }
+               return t;
+       }
+
+       /**
+        * Map a lookup type constant (integer) value to its name.
+        *
+        * @param type lookup type
+        * @return lookup type name
+        */
+       public static String getLookupTypeName(int type) {
+               String tn = null;
+               switch (type) {
+                       case GSUB_LOOKUP_TYPE_SINGLE:
+                               tn = "single";
+                               break;
+                       case GSUB_LOOKUP_TYPE_MULTIPLE:
+                               tn = "multiple";
+                               break;
+                       case GSUB_LOOKUP_TYPE_ALTERNATE:
+                               tn = "alternate";
+                               break;
+                       case GSUB_LOOKUP_TYPE_LIGATURE:
+                               tn = "ligature";
+                               break;
+                       case GSUB_LOOKUP_TYPE_CONTEXTUAL:
+                               tn = "contextual";
+                               break;
+                       case GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL:
+                               tn = "chainedcontextual";
+                               break;
+                       case GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION:
+                               tn = "extensionsubstitution";
+                               break;
+                       case GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE:
+                               tn = "reversechainiingcontextualsingle";
+                               break;
+                       default:
+                               tn = "unknown";
+                               break;
+               }
+               return tn;
+       }
+
+       /**
+        * Create a substitution subtable according to the specified arguments.
+        *
+        * @param type     subtable type
+        * @param id       subtable identifier
+        * @param sequence subtable sequence
+        * @param flags    subtable flags
+        * @param format   subtable format
+        * @param coverage subtable coverage table
+        * @param entries  subtable entries
+        * @return a glyph subtable instance
+        */
+       public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format,
+                                                                                          GlyphCoverageTable coverage, List entries) {
+               GlyphSubtable st = null;
+               switch (type) {
+                       case GSUB_LOOKUP_TYPE_SINGLE:
+                               st = SingleSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GSUB_LOOKUP_TYPE_MULTIPLE:
+                               st = MultipleSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GSUB_LOOKUP_TYPE_ALTERNATE:
+                               st = AlternateSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GSUB_LOOKUP_TYPE_LIGATURE:
+                               st = LigatureSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GSUB_LOOKUP_TYPE_CONTEXTUAL:
+                               st = ContextualSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL:
+                               st = ChainedContextualSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       case GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE:
+                               st = ReverseChainedSingleSubtable.create(id, sequence, flags, format, coverage, entries);
+                               break;
+                       default:
+                               break;
+               }
+               return st;
+       }
+
+       /**
+        * Create a substitution subtable according to the specified arguments.
+        *
+        * @param type     subtable type
+        * @param id       subtable identifier
+        * @param sequence subtable sequence
+        * @param flags    subtable flags
+        * @param format   subtable format
+        * @param coverage list of coverage table entries
+        * @param entries  subtable entries
+        * @return a glyph subtable instance
+        */
+       public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format, List coverage,
+                                                                                          List entries) {
+               return createSubtable(type, id, sequence, flags, format, GlyphCoverageTable.createCoverageTable(coverage), entries);
+       }
+
+       private abstract static class SingleSubtable extends GlyphSubstitutionSubtable {
+
+               SingleSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GSUB_LOOKUP_TYPE_SINGLE;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof SingleSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean substitute(GlyphSubstitutionState ss) {
+                       int gi = ss.getGlyph();
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) < 0) {
+                               return false;
+                       } else {
+                               int go = getGlyphForCoverageIndex(ci, gi);
+                               if ((go < 0) || (go > 65535)) {
+                                       go = 65535;
+                               }
+                               ss.putGlyph(go, ss.getAssociation(), Boolean.TRUE);
+                               ss.consume(1);
+                               return true;
+                       }
+               }
+
+               /**
+                * Obtain glyph for coverage index.
+                *
+                * @param ci coverage index
+                * @param gi original glyph index
+                * @return substituted glyph value
+                * @throws IllegalArgumentException if coverage index is not valid
+                */
+               public abstract int getGlyphForCoverageIndex(int ci, int gi) throws IllegalArgumentException;
+
+               static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                               List entries) {
+                       if (format == 1) {
+                               return new SingleSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 2) {
+                               return new SingleSubtableFormat2(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class SingleSubtableFormat1 extends SingleSubtable {
+
+               private int delta;
+               private int ciMax;
+
+               SingleSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       List entries = new ArrayList(1);
+                       entries.add(Integer.valueOf(delta));
+                       return entries;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getGlyphForCoverageIndex(int ci, int gi) throws IllegalArgumentException {
+                       if (ci <= ciMax) {
+                               return gi + delta;
+                       } else {
+                               throw new IllegalArgumentException(
+                                               "coverage index " + ci + " out of range, maximum coverage index is " + ciMax);
+                       }
+               }
+
+               private void populate(List entries) {
+                       if ((entries == null) || (entries.size() != 1)) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, must be non-null and contain exactly one entry");
+                       } else {
+                               Object o = entries.get(0);
+                               int delta = 0;
+                               if (o instanceof Integer) {
+                                       delta = ((Integer) o).intValue();
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException("illegal entries entry, must be Integer, but is: " + o);
+                               }
+                               this.delta = delta;
+                               this.ciMax = getCoverageSize() - 1;
+                       }
+               }
+       }
+
+       private static class SingleSubtableFormat2 extends SingleSubtable {
+
+               private int[] glyphs;
+
+               SingleSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       List entries = new ArrayList(glyphs.length);
+                       for (int i = 0, n = glyphs.length; i < n; i++) {
+                               entries.add(Integer.valueOf(glyphs[i]));
+                       }
+                       return entries;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getGlyphForCoverageIndex(int ci, int gi) throws IllegalArgumentException {
+                       if (glyphs == null) {
+                               return -1;
+                       } else if (ci >= glyphs.length) {
+                               throw new IllegalArgumentException(
+                                               "coverage index " + ci + " out of range, maximum coverage index is " + glyphs.length);
+                       } else {
+                               return glyphs[ci];
+                       }
+               }
+
+               private void populate(List entries) {
+                       int i = 0;
+                       int n = entries.size();
+                       int[] glyphs = new int[n];
+                       for (Iterator it = entries.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (o instanceof Integer) {
+                                       int gid = ((Integer) o).intValue();
+                                       if ((gid >= 0) && (gid < 65536)) {
+                                               glyphs[i++] = gid;
+                                       } else {
+                                               throw new AdvancedTypographicTableFormatException("illegal glyph index: " + gid);
+                                       }
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException("illegal entries entry, must be Integer: " + o);
+                               }
+                       }
+                       assert i == n;
+                       assert this.glyphs == null;
+                       this.glyphs = glyphs;
+               }
+       }
+
+       private abstract static class MultipleSubtable extends GlyphSubstitutionSubtable {
+
+               public MultipleSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GSUB_LOOKUP_TYPE_MULTIPLE;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof MultipleSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean substitute(GlyphSubstitutionState ss) {
+                       int gi = ss.getGlyph();
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) < 0) {
+                               return false;
+                       } else {
+                               int[] ga = getGlyphsForCoverageIndex(ci, gi);
+                               if (ga != null) {
+                                       ss.putGlyphs(ga, CharAssociation.replicate(ss.getAssociation(), ga.length), Boolean.TRUE);
+                                       ss.consume(1);
+                               }
+                               return true;
+                       }
+               }
+
+               /**
+                * Obtain glyph sequence for coverage index.
+                *
+                * @param ci coverage index
+                * @param gi original glyph index
+                * @return sequence of glyphs to substitute for input glyph
+                * @throws IllegalArgumentException if coverage index is not valid
+                */
+               public abstract int[] getGlyphsForCoverageIndex(int ci, int gi) throws IllegalArgumentException;
+
+               static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                               List entries) {
+                       if (format == 1) {
+                               return new MultipleSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class MultipleSubtableFormat1 extends MultipleSubtable {
+
+               private int[][] gsa;                            // glyph sequence array, ordered by coverage index
+
+               MultipleSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (gsa != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(gsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int[] getGlyphsForCoverageIndex(int ci, int gi) throws IllegalArgumentException {
+                       if (gsa == null) {
+                               return null;
+                       } else if (ci >= gsa.length) {
+                               throw new IllegalArgumentException(
+                                               "coverage index " + ci + " out of range, maximum coverage index is " + gsa.length);
+                       } else {
+                               return gsa[ci];
+                       }
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof int[][])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an int[][], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       gsa = (int[][]) o;
+                               }
+                       }
+               }
+       }
+
+       private abstract static class AlternateSubtable extends GlyphSubstitutionSubtable {
+
+               public AlternateSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GSUB_LOOKUP_TYPE_ALTERNATE;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof AlternateSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean substitute(GlyphSubstitutionState ss) {
+                       int gi = ss.getGlyph();
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) < 0) {
+                               return false;
+                       } else {
+                               int[] ga = getAlternatesForCoverageIndex(ci, gi);
+                               int ai = ss.getAlternatesIndex(ci);
+                               int go;
+                               if ((ai < 0) || (ai >= ga.length)) {
+                                       go = gi;
+                               } else {
+                                       go = ga[ai];
+                               }
+                               if ((go < 0) || (go > 65535)) {
+                                       go = 65535;
+                               }
+                               ss.putGlyph(go, ss.getAssociation(), Boolean.TRUE);
+                               ss.consume(1);
+                               return true;
+                       }
+               }
+
+               /**
+                * Obtain glyph alternates for coverage index.
+                *
+                * @param ci coverage index
+                * @param gi original glyph index
+                * @return sequence of glyphs to substitute for input glyph
+                * @throws IllegalArgumentException if coverage index is not valid
+                */
+               public abstract int[] getAlternatesForCoverageIndex(int ci, int gi) throws IllegalArgumentException;
+
+               static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                               List entries) {
+                       if (format == 1) {
+                               return new AlternateSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class AlternateSubtableFormat1 extends AlternateSubtable {
+
+               private int[][] gaa;                            // glyph alternates array, ordered by coverage index
+
+               AlternateSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       List entries = new ArrayList(gaa.length);
+                       for (int i = 0, n = gaa.length; i < n; i++) {
+                               entries.add(gaa[i]);
+                       }
+                       return entries;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int[] getAlternatesForCoverageIndex(int ci, int gi) throws IllegalArgumentException {
+                       if (gaa == null) {
+                               return null;
+                       } else if (ci >= gaa.length) {
+                               throw new IllegalArgumentException(
+                                               "coverage index " + ci + " out of range, maximum coverage index is " + gaa.length);
+                       } else {
+                               return gaa[ci];
+                       }
+               }
+
+               private void populate(List entries) {
+                       int i = 0;
+                       int n = entries.size();
+                       int[][] gaa = new int[n][];
+                       for (Iterator it = entries.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (o instanceof int[]) {
+                                       gaa[i++] = (int[]) o;
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException("illegal entries entry, must be int[]: " + o);
+                               }
+                       }
+                       assert i == n;
+                       assert this.gaa == null;
+                       this.gaa = gaa;
+               }
+       }
+
+       private abstract static class LigatureSubtable extends GlyphSubstitutionSubtable {
+
+               public LigatureSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GSUB_LOOKUP_TYPE_LIGATURE;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof LigatureSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean substitute(GlyphSubstitutionState ss) {
+                       int gi = ss.getGlyph();
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) < 0) {
+                               return false;
+                       } else {
+                               LigatureSet ls = getLigatureSetForCoverageIndex(ci, gi);
+                               if (ls != null) {
+                                       boolean reverse = false;
+                                       GlyphTester ignores = ss.getIgnoreDefault();
+                                       int[] counts = ss.getGlyphsAvailable(0, reverse, ignores);
+                                       int nga = counts[0];
+                                       int ngi;
+                                       if (nga > 1) {
+                                               int[] iga = ss.getGlyphs(0, nga, reverse, ignores, null, counts);
+                                               Ligature l = findLigature(ls, iga);
+                                               if (l != null) {
+                                                       int go = l.getLigature();
+                                                       if ((go < 0) || (go > 65535)) {
+                                                               go = 65535;
+                                                       }
+                                                       int nmg = 1 + l.getNumComponents();
+                                                       // fetch matched number of component glyphs to determine matched and ignored count
+                                                       ss.getGlyphs(0, nmg, reverse, ignores, null, counts);
+                                                       nga = counts[0];
+                                                       ngi = counts[1];
+                                                       // fetch associations of matched component glyphs
+                                                       CharAssociation[] laa = ss.getAssociations(0, nga);
+                                                       // output ligature glyph and its association
+                                                       ss.putGlyph(go, CharAssociation.join(laa), Boolean.TRUE);
+                                                       // fetch and output ignored glyphs (if necessary)
+                                                       if (ngi > 0) {
+                                                               ss.putGlyphs(ss.getIgnoredGlyphs(0, ngi), ss.getIgnoredAssociations(0, ngi), null);
+                                                       }
+                                                       ss.consume(nga + ngi);
+                                               }
+                                       }
+                               }
+                               return true;
+                       }
+               }
+
+               private Ligature findLigature(LigatureSet ls, int[] glyphs) {
+                       Ligature[] la = ls.getLigatures();
+                       int k = -1;
+                       int maxComponents = -1;
+                       for (int i = 0, n = la.length; i < n; i++) {
+                               Ligature l = la[i];
+                               if (l.matchesComponents(glyphs)) {
+                                       int nc = l.getNumComponents();
+                                       if (nc > maxComponents) {
+                                               maxComponents = nc;
+                                               k = i;
+                                       }
+                               }
+                       }
+                       if (k >= 0) {
+                               return la[k];
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * Obtain ligature set for coverage index.
+                *
+                * @param ci coverage index
+                * @param gi original glyph index
+                * @return ligature set (or null if none defined)
+                * @throws IllegalArgumentException if coverage index is not valid
+                */
+               public abstract LigatureSet getLigatureSetForCoverageIndex(int ci, int gi) throws IllegalArgumentException;
+
+               static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                               List entries) {
+                       if (format == 1) {
+                               return new LigatureSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class LigatureSubtableFormat1 extends LigatureSubtable {
+
+               private LigatureSet[] ligatureSets;
+
+               public LigatureSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                          List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       List entries = new ArrayList(ligatureSets.length);
+                       for (int i = 0, n = ligatureSets.length; i < n; i++) {
+                               entries.add(ligatureSets[i]);
+                       }
+                       return entries;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public LigatureSet getLigatureSetForCoverageIndex(int ci, int gi) throws IllegalArgumentException {
+                       if (ligatureSets == null) {
+                               return null;
+                       } else if (ci >= ligatureSets.length) {
+                               throw new IllegalArgumentException(
+                                               "coverage index " + ci + " out of range, maximum coverage index is " + ligatureSets.length);
+                       } else {
+                               return ligatureSets[ci];
+                       }
+               }
+
+               private void populate(List entries) {
+                       int i = 0;
+                       int n = entries.size();
+                       LigatureSet[] ligatureSets = new LigatureSet[n];
+                       for (Iterator it = entries.iterator(); it.hasNext(); ) {
+                               Object o = it.next();
+                               if (o instanceof LigatureSet) {
+                                       ligatureSets[i++] = (LigatureSet) o;
+                               } else {
+                                       throw new AdvancedTypographicTableFormatException("illegal ligatures entry, must be LigatureSet: " + o);
+                               }
+                       }
+                       assert i == n;
+                       assert this.ligatureSets == null;
+                       this.ligatureSets = ligatureSets;
+               }
+       }
+
+       private abstract static class ContextualSubtable extends GlyphSubstitutionSubtable {
+
+               public ContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                 List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GSUB_LOOKUP_TYPE_CONTEXTUAL;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof ContextualSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean substitute(GlyphSubstitutionState ss) {
+                       int gi = ss.getGlyph();
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) < 0) {
+                               return false;
+                       } else {
+                               int[] rv = new int[1];
+                               RuleLookup[] la = getLookups(ci, gi, ss, rv);
+                               if (la != null) {
+                                       ss.apply(la, rv[0]);
+                               }
+                               return true;
+                       }
+               }
+
+               /**
+                * Obtain rule lookups set associated current input glyph context.
+                *
+                * @param ci coverage index of glyph at current position
+                * @param gi glyph index of glyph at current position
+                * @param ss glyph substitution state
+                * @param rv array of ints used to receive multiple return values, must be of length 1 or greater,
+                *           where the first entry is used to return the input sequence length of the matched rule
+                * @return array of rule lookups or null if none applies
+                */
+               public abstract RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv);
+
+               static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                               List entries) {
+                       if (format == 1) {
+                               return new ContextualSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 2) {
+                               return new ContextualSubtableFormat2(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 3) {
+                               return new ContextualSubtableFormat3(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class ContextualSubtableFormat1 extends ContextualSubtable {
+
+               private RuleSet[] rsa;                          // rule set array, ordered by glyph coverage index
+
+               ContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                 List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) {
+                       assert ss != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedGlyphSequenceRule)) {
+                                                       ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r;
+                                                       int[] iga = cr.getGlyphs(gi);
+                                                       if (matches(ss, iga, 0, rv)) {
+                                                               return r.getLookups();
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               static boolean matches(GlyphSubstitutionState ss, int[] glyphs, int offset, int[] rv) {
+                       if ((glyphs == null) || (glyphs.length == 0)) {
+                               return true;                            // match null or empty glyph sequence
+                       } else {
+                               boolean reverse = offset < 0;
+                               GlyphTester ignores = ss.getIgnoreDefault();
+                               int[] counts = ss.getGlyphsAvailable(offset, reverse, ignores);
+                               int nga = counts[0];
+                               int ngm = glyphs.length;
+                               if (nga < ngm) {
+                                       return false;                       // insufficient glyphs available to match
+                               } else {
+                                       int[] ga = ss.getGlyphs(offset, ngm, reverse, ignores, null, counts);
+                                       for (int k = 0; k < ngm; k++) {
+                                               if (ga[k] != glyphs[k]) {
+                                                       return false;               // match fails at ga [ k ]
+                                               }
+                                       }
+                                       if (rv != null) {
+                                               rv[0] = counts[0] + counts[1];
+                                       }
+                                       return true;                        // all glyphs match
+                               }
+                       }
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                               }
+                       }
+               }
+       }
+
+       private static class ContextualSubtableFormat2 extends ContextualSubtable {
+
+               private GlyphClassTable cdt;                    // class def table
+               private int ngc;                                // class set count
+               private RuleSet[] rsa;                          // rule set array, ordered by class number [0...ngc - 1]
+
+               ContextualSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                 List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(3);
+                               entries.add(cdt);
+                               entries.add(Integer.valueOf(ngc));
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) {
+                       assert ss != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedClassSequenceRule)) {
+                                                       ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r;
+                                                       int[] ca = cr.getClasses(cdt.getClassIndex(gi, ss.getClassMatchSet(gi)));
+                                                       if (matches(ss, cdt, ca, 0, rv)) {
+                                                               return r.getLookups();
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               static boolean matches(GlyphSubstitutionState ss, GlyphClassTable cdt, int[] classes, int offset, int[] rv) {
+                       if ((cdt == null) || (classes == null) || (classes.length == 0)) {
+                               return true;                            // match null class definitions, null or empty class sequence
+                       } else {
+                               boolean reverse = offset < 0;
+                               GlyphTester ignores = ss.getIgnoreDefault();
+                               int[] counts = ss.getGlyphsAvailable(offset, reverse, ignores);
+                               int nga = counts[0];
+                               int ngm = classes.length;
+                               if (nga < ngm) {
+                                       return false;                       // insufficient glyphs available to match
+                               } else {
+                                       int[] ga = ss.getGlyphs(offset, ngm, reverse, ignores, null, counts);
+                                       for (int k = 0; k < ngm; k++) {
+                                               int gi = ga[k];
+                                               int ms = ss.getClassMatchSet(gi);
+                                               int gc = cdt.getClassIndex(gi, ms);
+                                               if ((gc < 0) || (gc >= cdt.getClassSize(ms))) {
+                                                       return false;               // none or invalid class fails mat ch
+                                               } else if (gc != classes[k]) {
+                                                       return false;               // match fails at ga [ k ]
+                                               }
+                                       }
+                                       if (rv != null) {
+                                               rv[0] = counts[0] + counts[1];
+                                       }
+                                       return true;                        // all glyphs match
+                               }
+                       }
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 3) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 3 entries");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof GlyphClassTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an GlyphClassTable, but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else {
+                                       cdt = (GlyphClassTable) o;
+                               }
+                               if (((o = entries.get(1)) == null) || !(o instanceof Integer)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, second entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       ngc = ((Integer) (o)).intValue();
+                               }
+                               if (((o = entries.get(2)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, third entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                                       if (rsa.length != ngc) {
+                                               throw new AdvancedTypographicTableFormatException(
+                                                               "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes");
+                                       }
+                               }
+                       }
+               }
+       }
+
+       private static class ContextualSubtableFormat3 extends ContextualSubtable {
+
+               private RuleSet[] rsa;                          // rule set array, containing a single rule set
+
+               ContextualSubtableFormat3(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                 List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) {
+                       assert ss != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedCoverageSequenceRule)) {
+                                                       ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r;
+                                                       GlyphCoverageTable[] gca = cr.getCoverages();
+                                                       if (matches(ss, gca, 0, rv)) {
+                                                               return r.getLookups();
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               static boolean matches(GlyphSubstitutionState ss, GlyphCoverageTable[] gca, int offset, int[] rv) {
+                       if ((gca == null) || (gca.length == 0)) {
+                               return true;                            // match null or empty coverage array
+                       } else {
+                               boolean reverse = offset < 0;
+                               GlyphTester ignores = ss.getIgnoreDefault();
+                               int[] counts = ss.getGlyphsAvailable(offset, reverse, ignores);
+                               int nga = counts[0];
+                               int ngm = gca.length;
+                               if (nga < ngm) {
+                                       return false;                       // insufficient glyphs available to match
+                               } else {
+                                       int[] ga = ss.getGlyphs(offset, ngm, reverse, ignores, null, counts);
+                                       for (int k = 0; k < ngm; k++) {
+                                               GlyphCoverageTable ct = gca[k];
+                                               if (ct != null) {
+                                                       if (ct.getCoverageIndex(ga[k]) < 0) {
+                                                               return false;           // match fails at ga [ k ]
+                                                       }
+                                               }
+                                       }
+                                       if (rv != null) {
+                                               rv[0] = counts[0] + counts[1];
+                                       }
+                                       return true;                        // all glyphs match
+                               }
+                       }
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                               }
+                       }
+               }
+       }
+
+       private abstract static class ChainedContextualSubtable extends GlyphSubstitutionSubtable {
+
+               public ChainedContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof ChainedContextualSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean substitute(GlyphSubstitutionState ss) {
+                       int gi = ss.getGlyph();
+                       int ci;
+                       if ((ci = getCoverageIndex(gi)) < 0) {
+                               return false;
+                       } else {
+                               int[] rv = new int[1];
+                               RuleLookup[] la = getLookups(ci, gi, ss, rv);
+                               if (la != null) {
+                                       ss.apply(la, rv[0]);
+                                       return true;
+                               } else {
+                                       return false;
+                               }
+                       }
+               }
+
+               /**
+                * Obtain rule lookups set associated current input glyph context.
+                *
+                * @param ci coverage index of glyph at current position
+                * @param gi glyph index of glyph at current position
+                * @param ss glyph substitution state
+                * @param rv array of ints used to receive multiple return values, must be of length 1 or greater
+                * @return array of rule lookups or null if none applies
+                */
+               public abstract RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv);
+
+               static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                               List entries) {
+                       if (format == 1) {
+                               return new ChainedContextualSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 2) {
+                               return new ChainedContextualSubtableFormat2(id, sequence, flags, format, coverage, entries);
+                       } else if (format == 3) {
+                               return new ChainedContextualSubtableFormat3(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class ChainedContextualSubtableFormat1 extends ChainedContextualSubtable {
+
+               private RuleSet[] rsa;                          // rule set array, ordered by glyph coverage index
+
+               ChainedContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) {
+                       assert ss != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedGlyphSequenceRule)) {
+                                                       ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r;
+                                                       int[] iga = cr.getGlyphs(gi);
+                                                       if (matches(ss, iga, 0, rv)) {
+                                                               int[] bga = cr.getBacktrackGlyphs();
+                                                               if (matches(ss, bga, -1, null)) {
+                                                                       int[] lga = cr.getLookaheadGlyphs();
+                                                                       if (matches(ss, lga, rv[0], null)) {
+                                                                               return r.getLookups();
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private boolean matches(GlyphSubstitutionState ss, int[] glyphs, int offset, int[] rv) {
+                       return ContextualSubtableFormat1.matches(ss, glyphs, offset, rv);
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                               }
+                       }
+               }
+       }
+
+       private static class ChainedContextualSubtableFormat2 extends ChainedContextualSubtable {
+
+               private GlyphClassTable icdt;                   // input class def table
+               private GlyphClassTable bcdt;                   // backtrack class def table
+               private GlyphClassTable lcdt;                   // lookahead class def table
+               private int ngc;                                // class set count
+               private RuleSet[] rsa;                          // rule set array, ordered by class number [0...ngc - 1]
+
+               ChainedContextualSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(5);
+                               entries.add(icdt);
+                               entries.add(bcdt);
+                               entries.add(lcdt);
+                               entries.add(Integer.valueOf(ngc));
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) {
+                       assert ss != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedClassSequenceRule)) {
+                                                       ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r;
+                                                       int[] ica = cr.getClasses(icdt.getClassIndex(gi, ss.getClassMatchSet(gi)));
+                                                       if (matches(ss, icdt, ica, 0, rv)) {
+                                                               int[] bca = cr.getBacktrackClasses();
+                                                               if (matches(ss, bcdt, bca, -1, null)) {
+                                                                       int[] lca = cr.getLookaheadClasses();
+                                                                       if (matches(ss, lcdt, lca, rv[0], null)) {
+                                                                               return r.getLookups();
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private boolean matches(GlyphSubstitutionState ss, GlyphClassTable cdt, int[] classes, int offset, int[] rv) {
+                       return ContextualSubtableFormat2.matches(ss, cdt, classes, offset, rv);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 5) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 5 entries");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof GlyphClassTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an GlyphClassTable, but is: " +
+                                                                       ((o != null) ? o.getClass() : null));
+                               } else {
+                                       icdt = (GlyphClassTable) o;
+                               }
+                               if (((o = entries.get(1)) != null) && !(o instanceof GlyphClassTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, second entry must be an GlyphClassTable, but is: " + o.getClass());
+                               } else {
+                                       bcdt = (GlyphClassTable) o;
+                               }
+                               if (((o = entries.get(2)) != null) && !(o instanceof GlyphClassTable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, third entry must be an GlyphClassTable, but is: " + o.getClass());
+                               } else {
+                                       lcdt = (GlyphClassTable) o;
+                               }
+                               if (((o = entries.get(3)) == null) || !(o instanceof Integer)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, fourth entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       ngc = ((Integer) (o)).intValue();
+                               }
+                               if (((o = entries.get(4)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, fifth entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                                       if (rsa.length != ngc) {
+                                               throw new AdvancedTypographicTableFormatException(
+                                                               "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes");
+                                       }
+                               }
+                       }
+               }
+       }
+
+       private static class ChainedContextualSubtableFormat3 extends ChainedContextualSubtable {
+
+               private RuleSet[] rsa;                          // rule set array, containing a single rule set
+
+               ChainedContextualSubtableFormat3(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       if (rsa != null) {
+                               List entries = new ArrayList(1);
+                               entries.add(rsa);
+                               return entries;
+                       } else {
+                               return null;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public void resolveLookupReferences(Map/*<String,LookupTable>*/ lookupTables) {
+                       GlyphTable.resolveLookupReferences(rsa, lookupTables);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) {
+                       assert ss != null;
+                       assert (rv != null) && (rv.length > 0);
+                       assert rsa != null;
+                       if (rsa.length > 0) {
+                               RuleSet rs = rsa[0];
+                               if (rs != null) {
+                                       Rule[] ra = rs.getRules();
+                                       for (int i = 0, n = ra.length; i < n; i++) {
+                                               Rule r = ra[i];
+                                               if ((r != null) && (r instanceof ChainedCoverageSequenceRule)) {
+                                                       ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r;
+                                                       GlyphCoverageTable[] igca = cr.getCoverages();
+                                                       if (matches(ss, igca, 0, rv)) {
+                                                               GlyphCoverageTable[] bgca = cr.getBacktrackCoverages();
+                                                               if (matches(ss, bgca, -1, null)) {
+                                                                       GlyphCoverageTable[] lgca = cr.getLookaheadCoverages();
+                                                                       if (matches(ss, lgca, rv[0], null)) {
+                                                                               return r.getLookups();
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       return null;
+               }
+
+               private boolean matches(GlyphSubstitutionState ss, GlyphCoverageTable[] gca, int offset, int[] rv) {
+                       return ContextualSubtableFormat3.matches(ss, gca, offset, rv);
+               }
+
+               private void populate(List entries) {
+                       if (entries == null) {
+                               throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
+                       } else if (entries.size() != 1) {
+                               throw new AdvancedTypographicTableFormatException(
+                                               "illegal entries, " + entries.size() + " entries present, but requires 1 entry");
+                       } else {
+                               Object o;
+                               if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
+                               } else {
+                                       rsa = (RuleSet[]) o;
+                               }
+                       }
+               }
+       }
+
+       private abstract static class ReverseChainedSingleSubtable extends GlyphSubstitutionSubtable {
+
+               public ReverseChainedSingleSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                       List entries) {
+                       super(id, sequence, flags, format, coverage);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int getType() {
+                       return GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean isCompatible(GlyphSubtable subtable) {
+                       return subtable instanceof ReverseChainedSingleSubtable;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean usesReverseScan() {
+                       return true;
+               }
+
+               static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                               List entries) {
+                       if (format == 1) {
+                               return new ReverseChainedSingleSubtableFormat1(id, sequence, flags, format, coverage, entries);
+                       } else {
+                               throw new UnsupportedOperationException();
+                       }
+               }
+       }
+
+       private static class ReverseChainedSingleSubtableFormat1 extends ReverseChainedSingleSubtable {
+
+               ReverseChainedSingleSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage,
+                                                                                       List entries) {
+                       super(id, sequence, flags, format, coverage, entries);
+                       populate(entries);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public List getEntries() {
+                       return null;
+               }
+
+               private void populate(List entries) {
+               }
+       }
+
+       /**
+        * The <code>Ligature</code> class implements a ligature lookup result in terms of
+        * a ligature glyph (code) and the <emph>N+1...</emph> components that comprise the ligature,
+        * where the <emph>Nth</emph> component was consumed in the coverage table lookup mapping to
+        * this ligature instance.
+        */
+       public static class Ligature {
+
+               private final int ligature;                     // (resulting) ligature glyph
+               private final int[] components;                 // component glyph codes (note that first component is implied)
+
+               /**
+                * Instantiate a ligature.
+                *
+                * @param ligature   glyph id
+                * @param components sequence of <emph>N+1...</emph> component glyph (or character) identifiers
+                */
+               public Ligature(int ligature, int[] components) {
+                       if ((ligature < 0) || (ligature > 65535)) {
+                               throw new AdvancedTypographicTableFormatException("invalid ligature glyph index: " + ligature);
+                       } else if (components == null) {
+                               throw new AdvancedTypographicTableFormatException("invalid ligature components, must be non-null array");
+                       } else {
+                               for (int i = 0, n = components.length; i < n; i++) {
+                                       int gc = components[i];
+                                       if ((gc < 0) || (gc > 65535)) {
+                                               throw new AdvancedTypographicTableFormatException("invalid component glyph index: " + gc);
+                                       }
+                               }
+                               this.ligature = ligature;
+                               this.components = components;
+                       }
+               }
+
+               /**
+                * @return ligature glyph id
+                */
+               public int getLigature() {
+                       return ligature;
+               }
+
+               /**
+                * @return array of <emph>N+1...</emph> components
+                */
+               public int[] getComponents() {
+                       return components;
+               }
+
+               /**
+                * @return components count
+                */
+               public int getNumComponents() {
+                       return components.length;
+               }
+
+               /**
+                * Determine if input sequence at offset matches ligature's components.
+                *
+                * @param glyphs array of glyph components to match (including first, implied glyph)
+                * @return true if matches
+                */
+               public boolean matchesComponents(int[] glyphs) {
+                       if (glyphs.length < (components.length + 1)) {
+                               return false;
+                       } else {
+                               for (int i = 0, n = components.length; i < n; i++) {
+                                       if (glyphs[i + 1] != components[i]) {
+                                               return false;
+                                       }
+                               }
+                               return true;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{components={");
+                       for (int i = 0, n = components.length; i < n; i++) {
+                               if (i > 0) {
+                                       sb.append(',');
+                               }
+                               sb.append(Integer.toString(components[i]));
+                       }
+                       sb.append("},ligature=");
+                       sb.append(Integer.toString(ligature));
+                       sb.append("}");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>LigatureSet</code> class implements a set of  ligatures.
+        */
+       public static class LigatureSet {
+
+               private final Ligature[] ligatures;
+               // set of ligatures all of which share the first (implied) component
+               private final int maxComponents;                        // maximum number of components (including first)
+
+               /**
+                * Instantiate a set of ligatures.
+                *
+                * @param ligatures collection of ligatures
+                */
+               public LigatureSet(List ligatures) {
+                       this((Ligature[]) ligatures.toArray(new Ligature[ligatures.size()]));
+               }
+
+               /**
+                * Instantiate a set of ligatures.
+                *
+                * @param ligatures array of ligatures
+                */
+               public LigatureSet(Ligature[] ligatures) {
+                       if (ligatures == null) {
+                               throw new AdvancedTypographicTableFormatException("invalid ligatures, must be non-null array");
+                       } else {
+                               this.ligatures = ligatures;
+                               int ncMax = -1;
+                               for (int i = 0, n = ligatures.length; i < n; i++) {
+                                       Ligature l = ligatures[i];
+                                       int nc = l.getNumComponents() + 1;
+                                       if (nc > ncMax) {
+                                               ncMax = nc;
+                                       }
+                               }
+                               maxComponents = ncMax;
+                       }
+               }
+
+               /**
+                * @return array of ligatures in this ligature set
+                */
+               public Ligature[] getLigatures() {
+                       return ligatures;
+               }
+
+               /**
+                * @return count of ligatures in this ligature set
+                */
+               public int getNumLigatures() {
+                       return ligatures.length;
+               }
+
+               /**
+                * @return maximum number of components in one ligature (including first component)
+                */
+               public int getMaxComponents() {
+                       return maxComponents;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{ligs={");
+                       for (int i = 0, n = ligatures.length; i < n; i++) {
+                               if (i > 0) {
+                                       sb.append(',');
+                               }
+                               sb.append(ligatures[i]);
+                       }
+                       sb.append("}}");
+                       return sb.toString();
+               }
+
+       }
+
+}
+
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubtable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubtable.java
new file mode 100644 (file)
index 0000000..65a66fa
--- /dev/null
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.*;
+import com.jaredrummler.fontreader.truetype.GlyphTable;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Map;
+
+// CSOFF: LineLengthCheck
+
+/**
+ * <p>The <code>GlyphSubtable</code> implements an abstract glyph subtable that
+ * encapsulates identification, type, format, and coverage information.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public abstract class GlyphSubtable implements Comparable {
+
+       /**
+        * lookup flag - right to left
+        */
+       public static final int LF_RIGHT_TO_LEFT = 0x0001;
+       /**
+        * lookup flag - ignore base glyphs
+        */
+       public static final int LF_IGNORE_BASE = 0x0002;
+       /**
+        * lookup flag - ignore ligatures
+        */
+       public static final int LF_IGNORE_LIGATURE = 0x0004;
+       /**
+        * lookup flag - ignore marks
+        */
+       public static final int LF_IGNORE_MARK = 0x0008;
+       /**
+        * lookup flag - use mark filtering set
+        */
+       public static final int LF_USE_MARK_FILTERING_SET = 0x0010;
+       /**
+        * lookup flag - reserved
+        */
+       public static final int LF_RESERVED = 0x0E00;
+       /**
+        * lookup flag - mark attachment type
+        */
+       public static final int LF_MARK_ATTACHMENT_TYPE = 0xFF00;
+       /**
+        * internal flag - use reverse scan
+        */
+       public static final int LF_INTERNAL_USE_REVERSE_SCAN = 0x10000;
+
+       /**
+        * lookup identifier, having form of "lu%d" where %d is index of lookup in lookup list; shared by multiple subtables
+        * in a single lookup
+        */
+       private String lookupId;
+       /**
+        * subtable sequence (index) number in lookup, zero based
+        */
+       private int sequence;
+       /**
+        * subtable flags
+        */
+       private int flags;
+       /**
+        * subtable format
+        */
+       private int format;
+       /**
+        * subtable mapping table
+        */
+       private GlyphMappingTable mapping;
+       /**
+        * weak reference to parent (gsub or gpos) table
+        */
+       private WeakReference table;
+
+       /**
+        * Instantiate this glyph subtable.
+        *
+        * @param lookupId lookup identifier, having form of "lu%d" where %d is index of lookup in lookup list
+        * @param sequence subtable sequence (within lookup), starting with zero
+        * @param flags    subtable flags
+        * @param format   subtable format
+        * @param mapping  subtable mapping table
+        */
+       protected GlyphSubtable(String lookupId, int sequence, int flags, int format, GlyphMappingTable mapping) {
+               if ((lookupId == null) || (lookupId.length() == 0)) {
+                       throw new AdvancedTypographicTableFormatException("invalid lookup identifier, must be non-empty string");
+               } else if (mapping == null) {
+                       throw new AdvancedTypographicTableFormatException("invalid mapping table, must not be null");
+               } else {
+                       this.lookupId = lookupId;
+                       this.sequence = sequence;
+                       this.flags = flags;
+                       this.format = format;
+                       this.mapping = mapping;
+               }
+       }
+
+       /**
+        * @return this subtable's lookup identifer
+        */
+       public String getLookupId() {
+               return lookupId;
+       }
+
+       /**
+        * @return this subtable's table type
+        */
+       public abstract int getTableType();
+
+       /**
+        * @return this subtable's type
+        */
+       public abstract int getType();
+
+       /**
+        * @return this subtable's type name
+        */
+       public abstract String getTypeName();
+
+       /**
+        * Determine if a glyph subtable is compatible with this glyph subtable. Two glyph subtables are
+        * compatible if the both may appear in a single lookup table.
+        *
+        * @param subtable a glyph subtable to determine compatibility
+        * @return true if specified subtable is compatible with this glyph subtable, where by compatible
+        * is meant that they share the same lookup type
+        */
+       public abstract boolean isCompatible(GlyphSubtable subtable);
+
+       /**
+        * @return true if subtable uses reverse scanning of glyph sequence, meaning from the last glyph
+        * in a glyph sequence to the first glyph
+        */
+       public abstract boolean usesReverseScan();
+
+       /**
+        * @return this subtable's sequence (index) within lookup
+        */
+       public int getSequence() {
+               return sequence;
+       }
+
+       /**
+        * @return this subtable's flags
+        */
+       public int getFlags() {
+               return flags;
+       }
+
+       /**
+        * @return this subtable's format
+        */
+       public int getFormat() {
+               return format;
+       }
+
+       /**
+        * @return this subtable's governing glyph definition table or null if none available
+        */
+       public GlyphDefinitionTable getGDEF() {
+               GlyphTable gt = getTable();
+               if (gt != null) {
+                       return gt.getGlyphDefinitions();
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * @return this subtable's coverage mapping or null if mapping is not a coverage mapping
+        */
+       public GlyphCoverageMapping getCoverage() {
+               if (mapping instanceof GlyphCoverageMapping) {
+                       return (GlyphCoverageMapping) mapping;
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * @return this subtable's class mapping or null if mapping is not a class mapping
+        */
+       public GlyphClassMapping getClasses() {
+               if (mapping instanceof GlyphClassMapping) {
+                       return (GlyphClassMapping) mapping;
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * @return this subtable's lookup entries
+        */
+       public abstract List getEntries();
+
+       /**
+        * @return this subtable's parent table (or null if undefined)
+        */
+       public synchronized GlyphTable getTable() {
+               WeakReference r = this.table;
+               return (r != null) ? (GlyphTable) r.get() : null;
+       }
+
+       /**
+        * Establish a weak reference from this subtable to its parent
+        * table. If table parameter is specified as <code>null</code>, then
+        * clear and remove weak reference.
+        *
+        * @param table the table or null
+        * @throws IllegalStateException if table is already set to non-null
+        */
+       public synchronized void setTable(GlyphTable table) throws IllegalStateException {
+               WeakReference r = this.table;
+               if (table == null) {
+                       this.table = null;
+                       if (r != null) {
+                               r.clear();
+                       }
+               } else if (r == null) {
+                       this.table = new WeakReference(table);
+               } else {
+                       throw new IllegalStateException("table already set");
+               }
+       }
+
+       /**
+        * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves.
+        *
+        * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+        */
+       public void resolveLookupReferences(Map/*<String,GlyphTable.LookupTable>*/ lookupTables) {
+       }
+
+       /**
+        * Map glyph id to coverage index.
+        *
+        * @param gid glyph id
+        * @return the corresponding coverage index of the specified glyph id
+        */
+       public int getCoverageIndex(int gid) {
+               if (mapping instanceof GlyphCoverageMapping) {
+                       return ((GlyphCoverageMapping) mapping).getCoverageIndex(gid);
+               } else {
+                       return -1;
+               }
+       }
+
+       /**
+        * Map glyph id to coverage index.
+        *
+        * @return the corresponding coverage index of the specified glyph id
+        */
+       public int getCoverageSize() {
+               if (mapping instanceof GlyphCoverageMapping) {
+                       return ((GlyphCoverageMapping) mapping).getCoverageSize();
+               } else {
+                       return 0;
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int hashCode() {
+               int hc = sequence;
+               hc = (hc * 3) + (lookupId.hashCode() ^ hc);
+               return hc;
+       }
+
+       /**
+        * {@inheritDoc}
+        *
+        * @return true if the lookup identifier and the sequence number of the specified subtable is the same
+        * as the lookup identifier and sequence number of this subtable
+        */
+       public boolean equals(Object o) {
+               if (o instanceof GlyphSubtable) {
+                       GlyphSubtable st = (GlyphSubtable) o;
+                       return lookupId.equals(st.lookupId) && (sequence == st.sequence);
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        *
+        * @return the result of comparing the lookup identifier and the sequence number of the specified subtable with
+        * the lookup identifier and sequence number of this subtable
+        */
+       public int compareTo(Object o) {
+               int d;
+               if (o instanceof GlyphSubtable) {
+                       GlyphSubtable st = (GlyphSubtable) o;
+                       if ((d = lookupId.compareTo(st.lookupId)) == 0) {
+                               if (sequence < st.sequence) {
+                                       d = -1;
+                               } else if (sequence > st.sequence) {
+                                       d = 1;
+                               }
+                       }
+               } else {
+                       d = -1;
+               }
+               return d;
+       }
+
+       /**
+        * Determine if any of the specified subtables uses reverse scanning.
+        *
+        * @param subtables array of glyph subtables
+        * @return true if any of the specified subtables uses reverse scanning.
+        */
+       public static boolean usesReverseScan(GlyphSubtable[] subtables) {
+               if ((subtables == null) || (subtables.length == 0)) {
+                       return false;
+               } else {
+                       for (int i = 0, n = subtables.length; i < n; i++) {
+                               if (subtables[i].usesReverseScan()) {
+                                       return true;
+                               }
+                       }
+                       return false;
+               }
+       }
+
+       /**
+        * Determine consistent flags for a set of subtables.
+        *
+        * @param subtables array of glyph subtables
+        * @return consistent flags
+        * @throws IllegalStateException if inconsistent flags
+        */
+       public static int getFlags(GlyphSubtable[] subtables) throws IllegalStateException {
+               if ((subtables == null) || (subtables.length == 0)) {
+                       return 0;
+               } else {
+                       int flags = 0;
+                       // obtain first non-zero value of flags in array of subtables
+                       for (int i = 0, n = subtables.length; i < n; i++) {
+                               int f = subtables[i].getFlags();
+                               if (flags == 0) {
+                                       flags = f;
+                                       break;
+                               }
+                       }
+                       // enforce flag consistency
+                       for (int i = 0, n = subtables.length; i < n; i++) {
+                               int f = subtables[i].getFlags();
+                               if (f != flags) {
+                                       throw new IllegalStateException("inconsistent lookup flags " + f + ", expected " + flags);
+                               }
+                       }
+                       return flags | (usesReverseScan(subtables) ? LF_INTERNAL_USE_REVERSE_SCAN : 0);
+               }
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Glyphs.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Glyphs.java
new file mode 100644 (file)
index 0000000..5611101
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+public class Glyphs {
+
+       /**
+        * The characters in WinAnsiEncoding
+        */
+       public static final char[] WINANSI_ENCODING = {
+                       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ' ', '\u0021',
+                       '\"', '\u0023', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '\u002d', '\u002e', '/', '0', '1', '2', '3', '4',
+                       '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
+                       'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '\u005b', '\\', '\u005d', '^', '_',
+                       '\u2018', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
+                       'v', 'w', 'x', 'y', 'z', '\u007b', '\u007c', '\u007d', '\u007e', '\u2022', '\u20ac', '\u2022', '\u201a', '\u0192',
+                       '\u201e', '\u2026', '\u2020', '\u2021', '\u02c6', '\u2030', '\u0160', '\u2039', '\u0152', '\u2022', '\u017d',
+                       '\u2022', '\u2022', '\u2018', '\u2019', '\u201c', '\u201d', '\u2022', '\u2013', '\u2014', '~', '\u2122', '\u0161',
+                       '\u203a', '\u0153', '\u2022', '\u017e', '\u0178', ' ', '\u00a1', '\u00a2', '\u00a3', '\u00a4', '\u00a5', '\u00a6',
+                       '\u00a7', '\u00a8', '\u00a9', '\u00aa', '\u00ab', '\u00ac', '\u00ad', '\u00ae', '\u00af', '\u00b0', '\u00b1',
+                       '\u00b2', '\u00b3', '\u00b4', '\u00b5', '\u00b6', '\u00b7', '\u00b8', '\u00b9', '\u00ba', '\u00bb', '\u00bc',
+                       '\u00bd', '\u00be', '\u00bf', '\u00c0', '\u00c1', '\u00c2', '\u00c3', '\u00c4', '\u00c5', '\u00c6', '\u00c7',
+                       '\u00c8', '\u00c9', '\u00ca', '\u00cb', '\u00cc', '\u00cd', '\u00ce', '\u00cf', '\u00d0', '\u00d1', '\u00d2',
+                       '\u00d3', '\u00d4', '\u00d5', '\u00d6', '\u00d7', '\u00d8', '\u00d9', '\u00da', '\u00db', '\u00dc', '\u00dd',
+                       '\u00de', '\u00df', '\u00e0', '\u00e1', '\u00e2', '\u00e3', '\u00e4', '\u00e5', '\u00e6', '\u00e7', '\u00e8',
+                       '\u00e9', '\u00ea', '\u00eb', '\u00ec', '\u00ed', '\u00ee', '\u00ef', '\u00f0', '\u00f1', '\u00f2', '\u00f3',
+                       '\u00f4', '\u00f5', '\u00f6', '\u00f7', '\u00f8', '\u00f9', '\u00fa', '\u00fb', '\u00fc', '\u00fd', '\u00fe',
+                       '\u00ff'
+       };
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFAdvancedTypographicTableReader.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFAdvancedTypographicTableReader.java
new file mode 100644 (file)
index 0000000..4e788f2
--- /dev/null
@@ -0,0 +1,3393 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.*;
+import com.jaredrummler.fontreader.truetype.*;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>OpenType Font (OTF) advanced typographic table reader. Used by @{Link org.apache.fop.fonts.truetype.TTFFile}
+ * to read advanced typographic tables (GDEF, GSUB, GPOS).</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public final class OTFAdvancedTypographicTableReader {
+
+       // instance state
+       private OpenFont otf;                                        // parent font file reader
+       private FontFileReader in;                                  // input reader
+       private GlyphDefinitionTable gdef;                          // glyph definition table
+       private GlyphSubstitutionTable gsub;                        // glyph substitution table
+       private GlyphPositioningTable gpos;                         // glyph positioning table
+       // transient parsing state
+       private transient Map/*<String,Object[3]>*/ seScripts;
+       // script-tag         => Object[3] : { default-language-tag, List(language-tag), seLanguages }
+       private transient Map/*<String,Object[2]>*/ seLanguages;
+       // language-tag       => Object[2] : { "f<required-feature-index>", List("f<feature-index>")
+       private transient Map/*<String,List<String>>*/ seFeatures;
+       // "f<feature-index>" => Object[2] : { feature-tag, List("lu<lookup-index>") }
+       private transient GlyphMappingTable seMapping;              // subtable entry mappings
+       private transient List seEntries;                           // subtable entry entries
+       private transient List seSubtables;                         // subtable entry subtables
+
+       /**
+        * Construct an <code>OTFAdvancedTypographicTableReader</code> instance.
+        *
+        * @param otf parent font file reader (must be non-null)
+        * @param in  font file reader (must be non-null)
+        */
+       public OTFAdvancedTypographicTableReader(OpenFont otf, FontFileReader in) {
+               assert otf != null;
+               assert in != null;
+               this.otf = otf;
+               this.in = in;
+       }
+
+       /**
+        * Read all advanced typographic tables.
+        *
+        * @throws AdvancedTypographicTableFormatException if ATT table has invalid format
+        */
+       public void readAll() throws AdvancedTypographicTableFormatException {
+               try {
+                       readGDEF();
+                       readGSUB();
+                       readGPOS();
+               } catch (AdvancedTypographicTableFormatException e) {
+                       resetATStateAll();
+                       throw e;
+               } catch (IOException e) {
+                       resetATStateAll();
+                       throw new AdvancedTypographicTableFormatException(e.getMessage(), e);
+               } finally {
+                       resetATState();
+               }
+       }
+
+       /**
+        * Determine if advanced (typographic) table is present.
+        *
+        * @return true if advanced (typographic) table is present
+        */
+       public boolean hasAdvancedTable() {
+               return (gdef != null) || (gsub != null) || (gpos != null);
+       }
+
+       /**
+        * Returns the GDEF table or null if none present.
+        *
+        * @return the GDEF table
+        */
+       public GlyphDefinitionTable getGDEF() {
+               return gdef;
+       }
+
+       /**
+        * Returns the GSUB table or null if none present.
+        *
+        * @return the GSUB table
+        */
+       public GlyphSubstitutionTable getGSUB() {
+               return gsub;
+       }
+
+       /**
+        * Returns the GPOS table or null if none present.
+        *
+        * @return the GPOS table
+        */
+       public GlyphPositioningTable getGPOS() {
+               return gpos;
+       }
+
+       private void readLangSysTable(OFTableName tableTag, long langSysTable, String langSysTag)
+                       throws IOException {
+               in.seekSet(langSysTable);
+               // read lookup order (reorder) table offset
+               int lo = in.readTTFUShort();
+               // read required feature index
+               int rf = in.readTTFUShort();
+               String rfi;
+               if (rf != 65535) {
+                       rfi = "f" + rf;
+               } else {
+                       rfi = null;
+               }
+               // read (non-required) feature count
+               int nf = in.readTTFUShort();
+               // dump info if debugging
+               // read (non-required) feature indices
+               int[] fia = new int[nf];
+               List fl = new java.util.ArrayList();
+               for (int i = 0; i < nf; i++) {
+                       int fi = in.readTTFUShort();
+                       fia[i] = fi;
+                       fl.add("f" + fi);
+               }
+               if (seLanguages == null) {
+                       seLanguages = new java.util.LinkedHashMap();
+               }
+               seLanguages.put(langSysTag, new Object[]{rfi, fl});
+       }
+
+       private static String defaultTag = "dflt";
+
+       private void readScriptTable(OFTableName tableTag, long scriptTable, String scriptTag) throws IOException {
+               in.seekSet(scriptTable);
+               // read default language system table offset
+               int dl = in.readTTFUShort();
+               String dt = defaultTag;
+               // read language system record count
+               int nl = in.readTTFUShort();
+               List ll = new java.util.ArrayList();
+               if (nl > 0) {
+                       String[] lta = new String[nl];
+                       int[] loa = new int[nl];
+                       // read language system records
+                       for (int i = 0, n = nl; i < n; i++) {
+                               String lt = in.readTTFString(4);
+                               int lo = in.readTTFUShort();
+                               lta[i] = lt;
+                               loa[i] = lo;
+                               if (dl == lo) {
+                                       dl = 0;
+                                       dt = lt;
+                               }
+                               ll.add(lt);
+                       }
+                       // read non-default language system tables
+                       for (int i = 0, n = nl; i < n; i++) {
+                               readLangSysTable(tableTag, scriptTable + loa[i], lta[i]);
+                       }
+               }
+               // read default language system table (if specified)
+               if (dl > 0) {
+                       readLangSysTable(tableTag, scriptTable + dl, dt);
+               }
+               seScripts.put(scriptTag, new Object[]{dt, ll, seLanguages});
+               seLanguages = null;
+       }
+
+       private void readScriptList(OFTableName tableTag, long scriptList) throws IOException {
+               in.seekSet(scriptList);
+               // read script record count
+               int ns = in.readTTFUShort();
+
+               if (ns > 0) {
+                       String[] sta = new String[ns];
+                       int[] soa = new int[ns];
+                       // read script records
+                       for (int i = 0, n = ns; i < n; i++) {
+                               String st = in.readTTFString(4);
+                               int so = in.readTTFUShort();
+
+                               sta[i] = st;
+                               soa[i] = so;
+                       }
+                       // read script tables
+                       for (int i = 0, n = ns; i < n; i++) {
+                               seLanguages = null;
+                               readScriptTable(tableTag, scriptList + soa[i], sta[i]);
+                       }
+               }
+       }
+
+       private void readFeatureTable(OFTableName tableTag, long featureTable, String featureTag, int featureIndex)
+                       throws IOException {
+               in.seekSet(featureTable);
+
+               // read feature params offset
+               int po = in.readTTFUShort();
+               // read lookup list indices count
+               int nl = in.readTTFUShort();
+               // dump info if debugging
+
+               // read lookup table indices
+               int[] lia = new int[nl];
+               List lul = new java.util.ArrayList();
+               for (int i = 0; i < nl; i++) {
+                       int li = in.readTTFUShort();
+
+                       lia[i] = li;
+                       lul.add("lu" + li);
+               }
+               seFeatures.put("f" + featureIndex, new Object[]{featureTag, lul});
+       }
+
+       private void readFeatureList(OFTableName tableTag, long featureList) throws IOException {
+               in.seekSet(featureList);
+               // read feature record count
+               int nf = in.readTTFUShort();
+
+               if (nf > 0) {
+                       String[] fta = new String[nf];
+                       int[] foa = new int[nf];
+                       // read feature records
+                       for (int i = 0, n = nf; i < n; i++) {
+                               String ft = in.readTTFString(4);
+                               int fo = in.readTTFUShort();
+
+                               fta[i] = ft;
+                               foa[i] = fo;
+                       }
+                       // read feature tables
+                       for (int i = 0, n = nf; i < n; i++) {
+                               readFeatureTable(tableTag, featureList + foa[i], fta[i], i);
+                       }
+               }
+       }
+
+       static final class GDEFLookupType {
+
+               static final int GLYPH_CLASS = 1;
+               static final int ATTACHMENT_POINT = 2;
+               static final int LIGATURE_CARET = 3;
+               static final int MARK_ATTACHMENT = 4;
+
+               private GDEFLookupType() {
+               }
+
+               public static int getSubtableType(int lt) {
+                       int st;
+                       switch (lt) {
+                               case GDEFLookupType.GLYPH_CLASS:
+                                       st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_GLYPH_CLASS;
+                                       break;
+                               case GDEFLookupType.ATTACHMENT_POINT:
+                                       st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_ATTACHMENT_POINT;
+                                       break;
+                               case GDEFLookupType.LIGATURE_CARET:
+                                       st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_LIGATURE_CARET;
+                                       break;
+                               case GDEFLookupType.MARK_ATTACHMENT:
+                                       st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_MARK_ATTACHMENT;
+                                       break;
+                               default:
+                                       st = -1;
+                                       break;
+                       }
+                       return st;
+               }
+
+               public static String toString(int type) {
+                       String s;
+                       switch (type) {
+                               case GLYPH_CLASS:
+                                       s = "GlyphClass";
+                                       break;
+                               case ATTACHMENT_POINT:
+                                       s = "AttachmentPoint";
+                                       break;
+                               case LIGATURE_CARET:
+                                       s = "LigatureCaret";
+                                       break;
+                               case MARK_ATTACHMENT:
+                                       s = "MarkAttachment";
+                                       break;
+                               default:
+                                       s = "?";
+                                       break;
+                       }
+                       return s;
+               }
+       }
+
+       static final class GSUBLookupType {
+
+               static final int SINGLE = 1;
+               static final int MULTIPLE = 2;
+               static final int ALTERNATE = 3;
+               static final int LIGATURE = 4;
+               static final int CONTEXTUAL = 5;
+               static final int CHAINED_CONTEXTUAL = 6;
+               static final int EXTENSION = 7;
+               static final int REVERSE_CHAINED_SINGLE = 8;
+
+               private GSUBLookupType() {
+               }
+
+               public static int getSubtableType(int lt) {
+                       int st;
+                       switch (lt) {
+                               case GSUBLookupType.SINGLE:
+                                       st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_SINGLE;
+                                       break;
+                               case GSUBLookupType.MULTIPLE:
+                                       st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_MULTIPLE;
+                                       break;
+                               case GSUBLookupType.ALTERNATE:
+                                       st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_ALTERNATE;
+                                       break;
+                               case GSUBLookupType.LIGATURE:
+                                       st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_LIGATURE;
+                                       break;
+                               case GSUBLookupType.CONTEXTUAL:
+                                       st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CONTEXTUAL;
+                                       break;
+                               case GSUBLookupType.CHAINED_CONTEXTUAL:
+                                       st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
+                                       break;
+                               case GSUBLookupType.EXTENSION:
+                                       st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION;
+                                       break;
+                               case GSUBLookupType.REVERSE_CHAINED_SINGLE:
+                                       st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE;
+                                       break;
+                               default:
+                                       st = -1;
+                                       break;
+                       }
+                       return st;
+               }
+
+               public static String toString(int type) {
+                       String s;
+                       switch (type) {
+                               case SINGLE:
+                                       s = "Single";
+                                       break;
+                               case MULTIPLE:
+                                       s = "Multiple";
+                                       break;
+                               case ALTERNATE:
+                                       s = "Alternate";
+                                       break;
+                               case LIGATURE:
+                                       s = "Ligature";
+                                       break;
+                               case CONTEXTUAL:
+                                       s = "Contextual";
+                                       break;
+                               case CHAINED_CONTEXTUAL:
+                                       s = "ChainedContextual";
+                                       break;
+                               case EXTENSION:
+                                       s = "Extension";
+                                       break;
+                               case REVERSE_CHAINED_SINGLE:
+                                       s = "ReverseChainedSingle";
+                                       break;
+                               default:
+                                       s = "?";
+                                       break;
+                       }
+                       return s;
+               }
+       }
+
+       static final class GPOSLookupType {
+
+               static final int SINGLE = 1;
+               static final int PAIR = 2;
+               static final int CURSIVE = 3;
+               static final int MARK_TO_BASE = 4;
+               static final int MARK_TO_LIGATURE = 5;
+               static final int MARK_TO_MARK = 6;
+               static final int CONTEXTUAL = 7;
+               static final int CHAINED_CONTEXTUAL = 8;
+               static final int EXTENSION = 9;
+
+               private GPOSLookupType() {
+               }
+
+               public static String toString(int type) {
+                       String s;
+                       switch (type) {
+                               case SINGLE:
+                                       s = "Single";
+                                       break;
+                               case PAIR:
+                                       s = "Pair";
+                                       break;
+                               case CURSIVE:
+                                       s = "Cursive";
+                                       break;
+                               case MARK_TO_BASE:
+                                       s = "MarkToBase";
+                                       break;
+                               case MARK_TO_LIGATURE:
+                                       s = "MarkToLigature";
+                                       break;
+                               case MARK_TO_MARK:
+                                       s = "MarkToMark";
+                                       break;
+                               case CONTEXTUAL:
+                                       s = "Contextual";
+                                       break;
+                               case CHAINED_CONTEXTUAL:
+                                       s = "ChainedContextual";
+                                       break;
+                               case EXTENSION:
+                                       s = "Extension";
+                                       break;
+                               default:
+                                       s = "?";
+                                       break;
+                       }
+                       return s;
+               }
+       }
+
+       static final class LookupFlag {
+
+               static final int RIGHT_TO_LEFT = 0x0001;
+               static final int IGNORE_BASE_GLYPHS = 0x0002;
+               static final int IGNORE_LIGATURE = 0x0004;
+               static final int IGNORE_MARKS = 0x0008;
+               static final int USE_MARK_FILTERING_SET = 0x0010;
+               static final int MARK_ATTACHMENT_TYPE = 0xFF00;
+
+               private LookupFlag() {
+               }
+
+               public static String toString(int flags) {
+                       StringBuffer sb = new StringBuffer();
+                       boolean first = true;
+                       if ((flags & RIGHT_TO_LEFT) != 0) {
+                               if (first) {
+                                       first = false;
+                               } else {
+                                       sb.append('|');
+                               }
+                               sb.append("RightToLeft");
+                       }
+                       if ((flags & IGNORE_BASE_GLYPHS) != 0) {
+                               if (first) {
+                                       first = false;
+                               } else {
+                                       sb.append('|');
+                               }
+                               sb.append("IgnoreBaseGlyphs");
+                       }
+                       if ((flags & IGNORE_LIGATURE) != 0) {
+                               if (first) {
+                                       first = false;
+                               } else {
+                                       sb.append('|');
+                               }
+                               sb.append("IgnoreLigature");
+                       }
+                       if ((flags & IGNORE_MARKS) != 0) {
+                               if (first) {
+                                       first = false;
+                               } else {
+                                       sb.append('|');
+                               }
+                               sb.append("IgnoreMarks");
+                       }
+                       if ((flags & USE_MARK_FILTERING_SET) != 0) {
+                               if (first) {
+                                       first = false;
+                               } else {
+                                       sb.append('|');
+                               }
+                               sb.append("UseMarkFilteringSet");
+                       }
+                       if (sb.length() == 0) {
+                               sb.append('-');
+                       }
+                       return sb.toString();
+               }
+       }
+
+       private GlyphCoverageTable readCoverageTableFormat1(String label, long tableOffset, int coverageFormat)
+                       throws IOException {
+               List entries = new java.util.ArrayList();
+               in.seekSet(tableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read glyph count
+               int ng = in.readTTFUShort();
+               int[] ga = new int[ng];
+               for (int i = 0, n = ng; i < n; i++) {
+                       int g = in.readTTFUShort();
+                       ga[i] = g;
+                       entries.add(Integer.valueOf(g));
+               }
+               // dump info if debugging
+               return GlyphCoverageTable.createCoverageTable(entries);
+       }
+
+       private GlyphCoverageTable readCoverageTableFormat2(String label, long tableOffset, int coverageFormat)
+                       throws IOException {
+               List entries = new java.util.ArrayList();
+               in.seekSet(tableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read range record count
+               int nr = in.readTTFUShort();
+               for (int i = 0, n = nr; i < n; i++) {
+                       // read range start
+                       int s = in.readTTFUShort();
+                       // read range end
+                       int e = in.readTTFUShort();
+                       // read range coverage (mapping) index
+                       int m = in.readTTFUShort();
+                       // dump info if debugging
+                       entries.add(new GlyphCoverageTable.MappingRange(s, e, m));
+               }
+               return GlyphCoverageTable.createCoverageTable(entries);
+       }
+
+       private GlyphCoverageTable readCoverageTable(String label, long tableOffset) throws IOException {
+               GlyphCoverageTable gct;
+               long cp = in.getCurrentPos();
+               in.seekSet(tableOffset);
+               // read coverage table format
+               int cf = in.readTTFUShort();
+               if (cf == 1) {
+                       gct = readCoverageTableFormat1(label, tableOffset, cf);
+               } else if (cf == 2) {
+                       gct = readCoverageTableFormat2(label, tableOffset, cf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported coverage table format: " + cf);
+               }
+               in.seekSet(cp);
+               return gct;
+       }
+
+       private GlyphClassTable readClassDefTableFormat1(String label, long tableOffset, int classFormat) throws IOException {
+               List entries = new java.util.ArrayList();
+               in.seekSet(tableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read start glyph
+               int sg = in.readTTFUShort();
+               entries.add(Integer.valueOf(sg));
+               // read glyph count
+               int ng = in.readTTFUShort();
+               // read glyph classes
+               int[] ca = new int[ng];
+               for (int i = 0, n = ng; i < n; i++) {
+                       int gc = in.readTTFUShort();
+                       ca[i] = gc;
+                       entries.add(Integer.valueOf(gc));
+               }
+               // dump info if debugging
+               return GlyphClassTable.createClassTable(entries);
+       }
+
+       private GlyphClassTable readClassDefTableFormat2(String label, long tableOffset, int classFormat) throws IOException {
+               List entries = new java.util.ArrayList();
+               in.seekSet(tableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read range record count
+               int nr = in.readTTFUShort();
+               for (int i = 0, n = nr; i < n; i++) {
+                       // read range start
+                       int s = in.readTTFUShort();
+                       // read range end
+                       int e = in.readTTFUShort();
+                       // read range glyph class (mapping) index
+                       int m = in.readTTFUShort();
+                       // dump info if debugging
+                       entries.add(new GlyphClassTable.MappingRange(s, e, m));
+               }
+               return GlyphClassTable.createClassTable(entries);
+       }
+
+       private GlyphClassTable readClassDefTable(String label, long tableOffset) throws IOException {
+               GlyphClassTable gct;
+               long cp = in.getCurrentPos();
+               in.seekSet(tableOffset);
+               // read class table format
+               int cf = in.readTTFUShort();
+               if (cf == 1) {
+                       gct = readClassDefTableFormat1(label, tableOffset, cf);
+               } else if (cf == 2) {
+                       gct = readClassDefTableFormat2(label, tableOffset, cf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported class definition table format: " + cf);
+               }
+               in.seekSet(cp);
+               return gct;
+       }
+
+       private void readSingleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read delta glyph
+               int dg = in.readTTFShort();
+               // dump info if debugging
+               // read coverage table
+               seMapping = readCoverageTable(tableTag + " single substitution coverage", subtableOffset + co);
+               seEntries.add(Integer.valueOf(dg));
+       }
+
+       private void readSingleSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read glyph count
+               int ng = in.readTTFUShort();
+               // dump info if debugging
+               // read coverage table
+               seMapping = readCoverageTable(tableTag + " single substitution coverage", subtableOffset + co);
+               // read glyph substitutions
+               int[] gsa = new int[ng];
+               for (int i = 0, n = ng; i < n; i++) {
+                       int gs = in.readTTFUShort();
+                       gsa[i] = gs;
+                       seEntries.add(Integer.valueOf(gs));
+               }
+       }
+
+       private int readSingleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read substitution subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readSingleSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 2) {
+                       readSingleSubTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported single substitution subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readMultipleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read sequence count
+               int ns = in.readTTFUShort();
+               // read coverage table
+               seMapping = readCoverageTable(tableTag + " multiple substitution coverage", subtableOffset + co);
+               // read sequence table offsets
+               int[] soa = new int[ns];
+               for (int i = 0, n = ns; i < n; i++) {
+                       soa[i] = in.readTTFUShort();
+               }
+               // read sequence tables
+               int[][] gsa = new int[ns][];
+               for (int i = 0, n = ns; i < n; i++) {
+                       int so = soa[i];
+                       int[] ga;
+                       if (so > 0) {
+                               in.seekSet(subtableOffset + so);
+                               // read glyph count
+                               int ng = in.readTTFUShort();
+                               ga = new int[ng];
+                               for (int j = 0; j < ng; j++) {
+                                       ga[j] = in.readTTFUShort();
+                               }
+                       } else {
+                               ga = null;
+                       }
+                       gsa[i] = ga;
+               }
+               seEntries.add(gsa);
+       }
+
+       private int readMultipleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read substitution subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readMultipleSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported multiple substitution subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readAlternateSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read alternate set count
+               int ns = in.readTTFUShort();
+
+               // read coverage table
+               seMapping = readCoverageTable(tableTag + " alternate substitution coverage", subtableOffset + co);
+               // read alternate set table offsets
+               int[] soa = new int[ns];
+               for (int i = 0, n = ns; i < n; i++) {
+                       soa[i] = in.readTTFUShort();
+               }
+               // read alternate set tables
+               for (int i = 0, n = ns; i < n; i++) {
+                       int so = soa[i];
+                       in.seekSet(subtableOffset + so);
+                       // read glyph count
+                       int ng = in.readTTFUShort();
+                       int[] ga = new int[ng];
+                       for (int j = 0; j < ng; j++) {
+                               int gs = in.readTTFUShort();
+                               ga[j] = gs;
+                       }
+                       seEntries.add(ga);
+               }
+       }
+
+       private int readAlternateSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read substitution subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readAlternateSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported alternate substitution subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readLigatureSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read ligature set count
+               int ns = in.readTTFUShort();
+               // read coverage table
+               seMapping = readCoverageTable(tableTag + " ligature substitution coverage", subtableOffset + co);
+               // read ligature set table offsets
+               int[] soa = new int[ns];
+               for (int i = 0, n = ns; i < n; i++) {
+                       soa[i] = in.readTTFUShort();
+               }
+               // read ligature set tables
+               for (int i = 0, n = ns; i < n; i++) {
+                       int so = soa[i];
+                       in.seekSet(subtableOffset + so);
+                       // read ligature table count
+                       int nl = in.readTTFUShort();
+                       int[] loa = new int[nl];
+                       for (int j = 0; j < nl; j++) {
+                               loa[j] = in.readTTFUShort();
+                       }
+                       List ligs = new java.util.ArrayList();
+                       for (int j = 0; j < nl; j++) {
+                               int lo = loa[j];
+                               in.seekSet(subtableOffset + so + lo);
+                               // read ligature glyph id
+                               int lg = in.readTTFUShort();
+                               // read ligature (input) component count
+                               int nc = in.readTTFUShort();
+                               int[] ca = new int[nc - 1];
+                               // read ligature (input) component glyph ids
+                               for (int k = 0; k < nc - 1; k++) {
+                                       ca[k] = in.readTTFUShort();
+                               }
+                               ligs.add(new GlyphSubstitutionTable.Ligature(lg, ca));
+                       }
+                       seEntries.add(new GlyphSubstitutionTable.LigatureSet(ligs));
+               }
+       }
+
+       private int readLigatureSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read substitution subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readLigatureSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported ligature substitution subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private GlyphTable.RuleLookup[] readRuleLookups(int numLookups, String header) throws IOException {
+               GlyphTable.RuleLookup[] la = new GlyphTable.RuleLookup[numLookups];
+               for (int i = 0, n = numLookups; i < n; i++) {
+                       int sequenceIndex = in.readTTFUShort();
+                       int lookupIndex = in.readTTFUShort();
+                       la[i] = new GlyphTable.RuleLookup(sequenceIndex, lookupIndex);
+               }
+               return la;
+       }
+
+       private void readContextualSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read rule set count
+               int nrs = in.readTTFUShort();
+               // read rule set offsets
+               int[] rsoa = new int[nrs];
+               for (int i = 0; i < nrs; i++) {
+                       rsoa[i] = in.readTTFUShort();
+               }
+               // read coverage table
+               GlyphCoverageTable ct;
+               if (co > 0) {
+                       ct = readCoverageTable(tableTag + " contextual substitution coverage", subtableOffset + co);
+               } else {
+                       ct = null;
+               }
+               // read rule sets
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[nrs];
+               String header = null;
+               for (int i = 0; i < nrs; i++) {
+                       GlyphTable.RuleSet rs;
+                       int rso = rsoa[i];
+                       if (rso > 0) {
+                               // seek to rule set [ i ]
+                               in.seekSet(subtableOffset + rso);
+                               // read rule count
+                               int nr = in.readTTFUShort();
+                               // read rule offsets
+                               int[] roa = new int[nr];
+                               GlyphTable.Rule[] ra = new GlyphTable.Rule[nr];
+                               for (int j = 0; j < nr; j++) {
+                                       roa[j] = in.readTTFUShort();
+                               }
+                               // read glyph sequence rules
+                               for (int j = 0; j < nr; j++) {
+                                       GlyphTable.GlyphSequenceRule r;
+                                       int ro = roa[j];
+                                       if (ro > 0) {
+                                               // seek to rule [ j ]
+                                               in.seekSet(subtableOffset + rso + ro);
+                                               // read glyph count
+                                               int ng = in.readTTFUShort();
+                                               // read rule lookup count
+                                               int nl = in.readTTFUShort();
+                                               // read glyphs
+                                               int[] glyphs = new int[ng - 1];
+                                               for (int k = 0, nk = glyphs.length; k < nk; k++) {
+                                                       glyphs[k] = in.readTTFUShort();
+                                               }
+                                               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+                                               r = new GlyphTable.GlyphSequenceRule(lookups, ng, glyphs);
+                                       } else {
+                                               r = null;
+                                       }
+                                       ra[j] = r;
+                               }
+                               rs = new GlyphTable.HomogeneousRuleSet(ra);
+                       } else {
+                               rs = null;
+                       }
+                       rsa[i] = rs;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(rsa);
+       }
+
+       private void readContextualSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read class def table offset
+               int cdo = in.readTTFUShort();
+               // read class rule set count
+               int ngc = in.readTTFUShort();
+               // read class rule set offsets
+               int[] csoa = new int[ngc];
+               for (int i = 0; i < ngc; i++) {
+                       csoa[i] = in.readTTFUShort();
+               }
+               // read coverage table
+               GlyphCoverageTable ct;
+               if (co > 0) {
+                       ct = readCoverageTable(tableTag + " contextual substitution coverage", subtableOffset + co);
+               } else {
+                       ct = null;
+               }
+               // read class definition table
+               GlyphClassTable cdt;
+               if (cdo > 0) {
+                       cdt = readClassDefTable(tableTag + " contextual substitution class definition", subtableOffset + cdo);
+               } else {
+                       cdt = null;
+               }
+               // read rule sets
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[ngc];
+               String header = null;
+               for (int i = 0; i < ngc; i++) {
+                       int cso = csoa[i];
+                       GlyphTable.RuleSet rs;
+                       if (cso > 0) {
+                               // seek to rule set [ i ]
+                               in.seekSet(subtableOffset + cso);
+                               // read rule count
+                               int nr = in.readTTFUShort();
+                               // read rule offsets
+                               int[] roa = new int[nr];
+                               GlyphTable.Rule[] ra = new GlyphTable.Rule[nr];
+                               for (int j = 0; j < nr; j++) {
+                                       roa[j] = in.readTTFUShort();
+                               }
+                               // read glyph sequence rules
+                               for (int j = 0; j < nr; j++) {
+                                       int ro = roa[j];
+                                       GlyphTable.ClassSequenceRule r;
+                                       if (ro > 0) {
+                                               // seek to rule [ j ]
+                                               in.seekSet(subtableOffset + cso + ro);
+                                               // read glyph count
+                                               int ng = in.readTTFUShort();
+                                               // read rule lookup count
+                                               int nl = in.readTTFUShort();
+                                               // read classes
+                                               int[] classes = new int[ng - 1];
+                                               for (int k = 0, nk = classes.length; k < nk; k++) {
+                                                       classes[k] = in.readTTFUShort();
+                                               }
+                                               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+                                               r = new GlyphTable.ClassSequenceRule(lookups, ng, classes);
+                                       } else {
+                                               assert ro > 0 : "unexpected null subclass rule offset";
+                                               r = null;
+                                       }
+                                       ra[j] = r;
+                               }
+                               rs = new GlyphTable.HomogeneousRuleSet(ra);
+                       } else {
+                               rs = null;
+                       }
+                       rsa[i] = rs;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(cdt);
+               seEntries.add(Integer.valueOf(ngc));
+               seEntries.add(rsa);
+       }
+
+       private void readContextualSubTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read glyph (input sequence length) count
+               int ng = in.readTTFUShort();
+               // read substitution lookup count
+               int nl = in.readTTFUShort();
+               // read glyph coverage offsets, one per glyph input sequence length count
+               int[] gcoa = new int[ng];
+               for (int i = 0; i < ng; i++) {
+                       gcoa[i] = in.readTTFUShort();
+               }
+               // read coverage tables
+               GlyphCoverageTable[] gca = new GlyphCoverageTable[ng];
+               for (int i = 0; i < ng; i++) {
+                       int gco = gcoa[i];
+                       GlyphCoverageTable gct;
+                       if (gco > 0) {
+                               gct = readCoverageTable(tableTag + " contextual substitution coverage[" + i + "]", subtableOffset + gco);
+                       } else {
+                               gct = null;
+                       }
+                       gca[i] = gct;
+               }
+               // read rule lookups
+               String header = null;
+               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+               // construct rule, rule set, and rule set array
+               GlyphTable.Rule r = new GlyphTable.CoverageSequenceRule(lookups, ng, gca);
+               GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet(new GlyphTable.Rule[]{r});
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[]{rs};
+               // store results
+               assert (gca != null) && (gca.length > 0);
+               seMapping = gca[0];
+               seEntries.add(rsa);
+       }
+
+       private int readContextualSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read substitution subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readContextualSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 2) {
+                       readContextualSubTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 3) {
+                       readContextualSubTableFormat3(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported contextual substitution subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readChainedContextualSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset,
+                                                                                                         int subtableFormat) throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read rule set count
+               int nrs = in.readTTFUShort();
+               // read rule set offsets
+               int[] rsoa = new int[nrs];
+               for (int i = 0; i < nrs; i++) {
+                       rsoa[i] = in.readTTFUShort();
+               }
+               // read coverage table
+               GlyphCoverageTable ct;
+               if (co > 0) {
+                       ct = readCoverageTable(tableTag + " chained contextual substitution coverage", subtableOffset + co);
+               } else {
+                       ct = null;
+               }
+               // read rule sets
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[nrs];
+               String header = null;
+               for (int i = 0; i < nrs; i++) {
+                       GlyphTable.RuleSet rs;
+                       int rso = rsoa[i];
+                       if (rso > 0) {
+                               // seek to rule set [ i ]
+                               in.seekSet(subtableOffset + rso);
+                               // read rule count
+                               int nr = in.readTTFUShort();
+                               // read rule offsets
+                               int[] roa = new int[nr];
+                               GlyphTable.Rule[] ra = new GlyphTable.Rule[nr];
+                               for (int j = 0; j < nr; j++) {
+                                       roa[j] = in.readTTFUShort();
+                               }
+                               // read glyph sequence rules
+                               for (int j = 0; j < nr; j++) {
+                                       GlyphTable.ChainedGlyphSequenceRule r;
+                                       int ro = roa[j];
+                                       if (ro > 0) {
+                                               // seek to rule [ j ]
+                                               in.seekSet(subtableOffset + rso + ro);
+                                               // read backtrack glyph count
+                                               int nbg = in.readTTFUShort();
+                                               // read backtrack glyphs
+                                               int[] backtrackGlyphs = new int[nbg];
+                                               for (int k = 0, nk = backtrackGlyphs.length; k < nk; k++) {
+                                                       backtrackGlyphs[k] = in.readTTFUShort();
+                                               }
+                                               // read input glyph count
+                                               int nig = in.readTTFUShort();
+                                               // read glyphs
+                                               int[] glyphs = new int[nig - 1];
+                                               for (int k = 0, nk = glyphs.length; k < nk; k++) {
+                                                       glyphs[k] = in.readTTFUShort();
+                                               }
+                                               // read lookahead glyph count
+                                               int nlg = in.readTTFUShort();
+                                               // read lookahead glyphs
+                                               int[] lookaheadGlyphs = new int[nlg];
+                                               for (int k = 0, nk = lookaheadGlyphs.length; k < nk; k++) {
+                                                       lookaheadGlyphs[k] = in.readTTFUShort();
+                                               }
+                                               // read rule lookup count
+                                               int nl = in.readTTFUShort();
+                                               // read rule lookups
+                                               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+                                               r = new GlyphTable.ChainedGlyphSequenceRule(lookups, nig, glyphs, backtrackGlyphs, lookaheadGlyphs);
+                                       } else {
+                                               r = null;
+                                       }
+                                       ra[j] = r;
+                               }
+                               rs = new GlyphTable.HomogeneousRuleSet(ra);
+                       } else {
+                               rs = null;
+                       }
+                       rsa[i] = rs;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(rsa);
+       }
+
+       private void readChainedContextualSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset,
+                                                                                                         int subtableFormat) throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read backtrack class def table offset
+               int bcdo = in.readTTFUShort();
+               // read input class def table offset
+               int icdo = in.readTTFUShort();
+               // read lookahead class def table offset
+               int lcdo = in.readTTFUShort();
+               // read class set count
+               int ngc = in.readTTFUShort();
+               // read class set offsets
+               int[] csoa = new int[ngc];
+               for (int i = 0; i < ngc; i++) {
+                       csoa[i] = in.readTTFUShort();
+               }
+               // read coverage table
+               GlyphCoverageTable ct;
+               if (co > 0) {
+                       ct = readCoverageTable(tableTag + " chained contextual substitution coverage", subtableOffset + co);
+               } else {
+                       ct = null;
+               }
+               // read backtrack class definition table
+               GlyphClassTable bcdt;
+               if (bcdo > 0) {
+                       bcdt = readClassDefTable(tableTag + " contextual substitution backtrack class definition", subtableOffset + bcdo);
+               } else {
+                       bcdt = null;
+               }
+               // read input class definition table
+               GlyphClassTable icdt;
+               if (icdo > 0) {
+                       icdt = readClassDefTable(tableTag + " contextual substitution input class definition", subtableOffset + icdo);
+               } else {
+                       icdt = null;
+               }
+               // read lookahead class definition table
+               GlyphClassTable lcdt;
+               if (lcdo > 0) {
+                       lcdt = readClassDefTable(tableTag + " contextual substitution lookahead class definition", subtableOffset + lcdo);
+               } else {
+                       lcdt = null;
+               }
+               // read rule sets
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[ngc];
+               String header = null;
+               for (int i = 0; i < ngc; i++) {
+                       int cso = csoa[i];
+                       GlyphTable.RuleSet rs;
+                       if (cso > 0) {
+                               // seek to rule set [ i ]
+                               in.seekSet(subtableOffset + cso);
+                               // read rule count
+                               int nr = in.readTTFUShort();
+                               // read rule offsets
+                               int[] roa = new int[nr];
+                               GlyphTable.Rule[] ra = new GlyphTable.Rule[nr];
+                               for (int j = 0; j < nr; j++) {
+                                       roa[j] = in.readTTFUShort();
+                               }
+                               // read glyph sequence rules
+                               for (int j = 0; j < nr; j++) {
+                                       int ro = roa[j];
+                                       GlyphTable.ChainedClassSequenceRule r;
+                                       if (ro > 0) {
+                                               // seek to rule [ j ]
+                                               in.seekSet(subtableOffset + cso + ro);
+                                               // read backtrack glyph class count
+                                               int nbc = in.readTTFUShort();
+                                               // read backtrack glyph classes
+                                               int[] backtrackClasses = new int[nbc];
+                                               for (int k = 0, nk = backtrackClasses.length; k < nk; k++) {
+                                                       backtrackClasses[k] = in.readTTFUShort();
+                                               }
+                                               // read input glyph class count
+                                               int nic = in.readTTFUShort();
+                                               // read input glyph classes
+                                               int[] classes = new int[nic - 1];
+                                               for (int k = 0, nk = classes.length; k < nk; k++) {
+                                                       classes[k] = in.readTTFUShort();
+                                               }
+                                               // read lookahead glyph class count
+                                               int nlc = in.readTTFUShort();
+                                               // read lookahead glyph classes
+                                               int[] lookaheadClasses = new int[nlc];
+                                               for (int k = 0, nk = lookaheadClasses.length; k < nk; k++) {
+                                                       lookaheadClasses[k] = in.readTTFUShort();
+                                               }
+                                               // read rule lookup count
+                                               int nl = in.readTTFUShort();
+                                               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+                                               r = new GlyphTable.ChainedClassSequenceRule(lookups, nic, classes, backtrackClasses, lookaheadClasses);
+                                       } else {
+                                               r = null;
+                                       }
+                                       ra[j] = r;
+                               }
+                               rs = new GlyphTable.HomogeneousRuleSet(ra);
+                       } else {
+                               rs = null;
+                       }
+                       rsa[i] = rs;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(icdt);
+               seEntries.add(bcdt);
+               seEntries.add(lcdt);
+               seEntries.add(Integer.valueOf(ngc));
+               seEntries.add(rsa);
+       }
+
+       private void readChainedContextualSubTableFormat3(int lookupType, int lookupFlags, long subtableOffset,
+                                                                                                         int subtableFormat) throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read backtrack glyph count
+               int nbg = in.readTTFUShort();
+               // read backtrack glyph coverage offsets
+               int[] bgcoa = new int[nbg];
+               for (int i = 0; i < nbg; i++) {
+                       bgcoa[i] = in.readTTFUShort();
+               }
+               // read input glyph count
+               int nig = in.readTTFUShort();
+               // read input glyph coverage offsets
+               int[] igcoa = new int[nig];
+               for (int i = 0; i < nig; i++) {
+                       igcoa[i] = in.readTTFUShort();
+               }
+               // read lookahead glyph count
+               int nlg = in.readTTFUShort();
+               // read lookahead glyph coverage offsets
+               int[] lgcoa = new int[nlg];
+               for (int i = 0; i < nlg; i++) {
+                       lgcoa[i] = in.readTTFUShort();
+               }
+               // read substitution lookup count
+               int nl = in.readTTFUShort();
+               // read backtrack coverage tables
+               GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg];
+               for (int i = 0; i < nbg; i++) {
+                       int bgco = bgcoa[i];
+                       GlyphCoverageTable bgct;
+                       if (bgco > 0) {
+                               bgct = readCoverageTable(tableTag + " chained contextual substitution backtrack coverage[" + i + "]",
+                                               subtableOffset + bgco);
+                       } else {
+                               bgct = null;
+                       }
+                       bgca[i] = bgct;
+               }
+               // read input coverage tables
+               GlyphCoverageTable[] igca = new GlyphCoverageTable[nig];
+               for (int i = 0; i < nig; i++) {
+                       int igco = igcoa[i];
+                       GlyphCoverageTable igct;
+                       if (igco > 0) {
+                               igct = readCoverageTable(tableTag + " chained contextual substitution input coverage[" + i + "]",
+                                               subtableOffset + igco);
+                       } else {
+                               igct = null;
+                       }
+                       igca[i] = igct;
+               }
+               // read lookahead coverage tables
+               GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg];
+               for (int i = 0; i < nlg; i++) {
+                       int lgco = lgcoa[i];
+                       GlyphCoverageTable lgct;
+                       if (lgco > 0) {
+                               lgct = readCoverageTable(tableTag + " chained contextual substitution lookahead coverage[" + i + "]",
+                                               subtableOffset + lgco);
+                       } else {
+                               lgct = null;
+                       }
+                       lgca[i] = lgct;
+               }
+               // read rule lookups
+               String header = null;
+               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+               // construct rule, rule set, and rule set array
+               GlyphTable.Rule r = new GlyphTable.ChainedCoverageSequenceRule(lookups, nig, igca, bgca, lgca);
+               GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet(new GlyphTable.Rule[]{r});
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[]{rs};
+               // store results
+               assert (igca != null) && (igca.length > 0);
+               seMapping = igca[0];
+               seEntries.add(rsa);
+       }
+
+       private int readChainedContextualSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read substitution subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readChainedContextualSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 2) {
+                       readChainedContextualSubTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 3) {
+                       readChainedContextualSubTableFormat3(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException(
+                                       "unsupported chained contextual substitution subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readExtensionSubTableFormat1(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence,
+                                                                                         long subtableOffset, int subtableFormat) throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read extension lookup type
+               int lt = in.readTTFUShort();
+               // read extension offset
+               long eo = in.readTTFULong();
+               // read referenced subtable from extended offset
+               readGSUBSubtable(lt, lookupFlags, lookupSequence, subtableSequence, subtableOffset + eo);
+       }
+
+       private int readExtensionSubTable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence,
+                                                                         long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read substitution subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readExtensionSubTableFormat1(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported extension substitution subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readReverseChainedSingleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset,
+                                                                                                                int subtableFormat) throws IOException {
+               String tableTag = "GSUB";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read backtrack glyph count
+               int nbg = in.readTTFUShort();
+               // read backtrack glyph coverage offsets
+               int[] bgcoa = new int[nbg];
+               for (int i = 0; i < nbg; i++) {
+                       bgcoa[i] = in.readTTFUShort();
+               }
+               // read lookahead glyph count
+               int nlg = in.readTTFUShort();
+               // read backtrack glyph coverage offsets
+               int[] lgcoa = new int[nlg];
+               for (int i = 0; i < nlg; i++) {
+                       lgcoa[i] = in.readTTFUShort();
+               }
+               // read substitution (output) glyph count
+               int ng = in.readTTFUShort();
+               // read substitution (output) glyphs
+               int[] glyphs = new int[ng];
+               for (int i = 0, n = ng; i < n; i++) {
+                       glyphs[i] = in.readTTFUShort();
+               }
+               // read coverage table
+               GlyphCoverageTable ct =
+                               readCoverageTable(tableTag + " reverse chained contextual substitution coverage", subtableOffset + co);
+               // read backtrack coverage tables
+               GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg];
+               for (int i = 0; i < nbg; i++) {
+                       int bgco = bgcoa[i];
+                       GlyphCoverageTable bgct;
+                       if (bgco > 0) {
+                               bgct = readCoverageTable(tableTag + " reverse chained contextual substitution backtrack coverage[" + i + "]",
+                                               subtableOffset + bgco);
+                       } else {
+                               bgct = null;
+                       }
+                       bgca[i] = bgct;
+               }
+               // read lookahead coverage tables
+               GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg];
+               for (int i = 0; i < nlg; i++) {
+                       int lgco = lgcoa[i];
+                       GlyphCoverageTable lgct;
+                       if (lgco > 0) {
+                               lgct = readCoverageTable(tableTag + " reverse chained contextual substitution lookahead coverage[" + i + "]",
+                                               subtableOffset + lgco);
+                       } else {
+                               lgct = null;
+                       }
+                       lgca[i] = lgct;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(bgca);
+               seEntries.add(lgca);
+               seEntries.add(glyphs);
+       }
+
+       private int readReverseChainedSingleSubTable(int lookupType, int lookupFlags, long subtableOffset)
+                       throws IOException {
+               in.seekSet(subtableOffset);
+               // read substitution subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readReverseChainedSingleSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException(
+                                       "unsupported reverse chained single substitution subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readGSUBSubtable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence,
+                                                                 long subtableOffset) throws IOException {
+               initATSubState();
+               int subtableFormat = -1;
+               switch (lookupType) {
+                       case GSUBLookupType.SINGLE:
+                               subtableFormat = readSingleSubTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GSUBLookupType.MULTIPLE:
+                               subtableFormat = readMultipleSubTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GSUBLookupType.ALTERNATE:
+                               subtableFormat = readAlternateSubTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GSUBLookupType.LIGATURE:
+                               subtableFormat = readLigatureSubTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GSUBLookupType.CONTEXTUAL:
+                               subtableFormat = readContextualSubTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GSUBLookupType.CHAINED_CONTEXTUAL:
+                               subtableFormat = readChainedContextualSubTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GSUBLookupType.REVERSE_CHAINED_SINGLE:
+                               subtableFormat = readReverseChainedSingleSubTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GSUBLookupType.EXTENSION:
+                               subtableFormat =
+                                               readExtensionSubTable(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset);
+                               break;
+                       default:
+                               break;
+               }
+               extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION, lookupType, lookupFlags, lookupSequence,
+                               subtableSequence, subtableFormat);
+               resetATSubState();
+       }
+
+       private GlyphPositioningTable.DeviceTable readPosDeviceTable(long subtableOffset, long deviceTableOffset)
+                       throws IOException {
+               long cp = in.getCurrentPos();
+               in.seekSet(subtableOffset + deviceTableOffset);
+               // read start size
+               int ss = in.readTTFUShort();
+               // read end size
+               int es = in.readTTFUShort();
+               // read delta format
+               int df = in.readTTFUShort();
+               int s1;
+               int m1;
+               int dm;
+               int dd;
+               int s2;
+               if (df == 1) {
+                       s1 = 14;
+                       m1 = 0x3;
+                       dm = 1;
+                       dd = 4;
+                       s2 = 2;
+               } else if (df == 2) {
+                       s1 = 12;
+                       m1 = 0xF;
+                       dm = 7;
+                       dd = 16;
+                       s2 = 4;
+               } else if (df == 3) {
+                       s1 = 8;
+                       m1 = 0xFF;
+                       dm = 127;
+                       dd = 256;
+                       s2 = 8;
+               } else {
+                       return null;
+               }
+               // read deltas
+               int n = (es - ss) + 1;
+               if (n < 0) {
+                       return null;
+               }
+               int[] da = new int[n];
+               for (int i = 0; (i < n) && (s2 > 0); ) {
+                       int p = in.readTTFUShort();
+                       for (int j = 0, k = 16 / s2; j < k; j++) {
+                               int d = (p >> s1) & m1;
+                               if (d > dm) {
+                                       d -= dd;
+                               }
+                               if (i < n) {
+                                       da[i++] = d;
+                               } else {
+                                       break;
+                               }
+                               p <<= s2;
+                       }
+               }
+               in.seekSet(cp);
+               return new GlyphPositioningTable.DeviceTable(ss, es, da);
+       }
+
+       private GlyphPositioningTable.Value readPosValue(long subtableOffset, int valueFormat) throws IOException {
+               // XPlacement
+               int xp;
+               if ((valueFormat & GlyphPositioningTable.Value.X_PLACEMENT) != 0) {
+                       xp = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
+               } else {
+                       xp = 0;
+               }
+               // YPlacement
+               int yp;
+               if ((valueFormat & GlyphPositioningTable.Value.Y_PLACEMENT) != 0) {
+                       yp = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
+               } else {
+                       yp = 0;
+               }
+               // XAdvance
+               int xa;
+               if ((valueFormat & GlyphPositioningTable.Value.X_ADVANCE) != 0) {
+                       xa = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
+               } else {
+                       xa = 0;
+               }
+               // YAdvance
+               int ya;
+               if ((valueFormat & GlyphPositioningTable.Value.Y_ADVANCE) != 0) {
+                       ya = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
+               } else {
+                       ya = 0;
+               }
+               // XPlaDevice
+               GlyphPositioningTable.DeviceTable xpd;
+               if ((valueFormat & GlyphPositioningTable.Value.X_PLACEMENT_DEVICE) != 0) {
+                       int xpdo = in.readTTFUShort();
+                       xpd = readPosDeviceTable(subtableOffset, xpdo);
+               } else {
+                       xpd = null;
+               }
+               // YPlaDevice
+               GlyphPositioningTable.DeviceTable ypd;
+               if ((valueFormat & GlyphPositioningTable.Value.Y_PLACEMENT_DEVICE) != 0) {
+                       int ypdo = in.readTTFUShort();
+                       ypd = readPosDeviceTable(subtableOffset, ypdo);
+               } else {
+                       ypd = null;
+               }
+               // XAdvDevice
+               GlyphPositioningTable.DeviceTable xad;
+               if ((valueFormat & GlyphPositioningTable.Value.X_ADVANCE_DEVICE) != 0) {
+                       int xado = in.readTTFUShort();
+                       xad = readPosDeviceTable(subtableOffset, xado);
+               } else {
+                       xad = null;
+               }
+               // YAdvDevice
+               GlyphPositioningTable.DeviceTable yad;
+               if ((valueFormat & GlyphPositioningTable.Value.Y_ADVANCE_DEVICE) != 0) {
+                       int yado = in.readTTFUShort();
+                       yad = readPosDeviceTable(subtableOffset, yado);
+               } else {
+                       yad = null;
+               }
+               return new GlyphPositioningTable.Value(xp, yp, xa, ya, xpd, ypd, xad, yad);
+       }
+
+       private void readSinglePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read value format
+               int vf = in.readTTFUShort();
+               // read value
+               GlyphPositioningTable.Value v = readPosValue(subtableOffset, vf);
+               // read coverage table
+               GlyphCoverageTable ct = readCoverageTable(tableTag + " single positioning coverage", subtableOffset + co);
+               // store results
+               seMapping = ct;
+               seEntries.add(v);
+       }
+
+       private void readSinglePosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read value format
+               int vf = in.readTTFUShort();
+               // read value count
+               int nv = in.readTTFUShort();
+               // read coverage table
+               GlyphCoverageTable ct = readCoverageTable(tableTag + " single positioning coverage", subtableOffset + co);
+               // read positioning values
+               GlyphPositioningTable.Value[] pva = new GlyphPositioningTable.Value[nv];
+               for (int i = 0, n = nv; i < n; i++) {
+                       GlyphPositioningTable.Value pv = readPosValue(subtableOffset, vf);
+                       pva[i] = pv;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(pva);
+       }
+
+       private int readSinglePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read positionining subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readSinglePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 2) {
+                       readSinglePosTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported single positioning subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private GlyphPositioningTable.PairValues readPosPairValues(long subtableOffset, boolean hasGlyph, int vf1, int vf2)
+                       throws IOException {
+               // read glyph (if present)
+               int glyph;
+               if (hasGlyph) {
+                       glyph = in.readTTFUShort();
+               } else {
+                       glyph = 0;
+               }
+               // read first value (if present)
+               GlyphPositioningTable.Value v1;
+               if (vf1 != 0) {
+                       v1 = readPosValue(subtableOffset, vf1);
+               } else {
+                       v1 = null;
+               }
+               // read second value (if present)
+               GlyphPositioningTable.Value v2;
+               if (vf2 != 0) {
+                       v2 = readPosValue(subtableOffset, vf2);
+               } else {
+                       v2 = null;
+               }
+               return new GlyphPositioningTable.PairValues(glyph, v1, v2);
+       }
+
+       private GlyphPositioningTable.PairValues[] readPosPairSetTable(long subtableOffset, int pairSetTableOffset, int vf1,
+                                                                                                                                  int vf2) throws IOException {
+               String tableTag = "GPOS";
+               long cp = in.getCurrentPos();
+               in.seekSet(subtableOffset + pairSetTableOffset);
+               // read pair values count
+               int npv = in.readTTFUShort();
+               // read pair values
+               GlyphPositioningTable.PairValues[] pva = new GlyphPositioningTable.PairValues[npv];
+               for (int i = 0, n = npv; i < n; i++) {
+                       GlyphPositioningTable.PairValues pv = readPosPairValues(subtableOffset, true, vf1, vf2);
+                       pva[i] = pv;
+               }
+               in.seekSet(cp);
+               return pva;
+       }
+
+       private void readPairPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read value format for first glyph
+               int vf1 = in.readTTFUShort();
+               // read value format for second glyph
+               int vf2 = in.readTTFUShort();
+               // read number (count) of pair sets
+               int nps = in.readTTFUShort();
+               // read coverage table
+               GlyphCoverageTable ct = readCoverageTable(tableTag + " pair positioning coverage", subtableOffset + co);
+               // read pair value matrix
+               GlyphPositioningTable.PairValues[][] pvm = new GlyphPositioningTable.PairValues[nps][];
+               for (int i = 0, n = nps; i < n; i++) {
+                       // read pair set offset
+                       int pso = in.readTTFUShort();
+                       // read pair set table at offset
+                       pvm[i] = readPosPairSetTable(subtableOffset, pso, vf1, vf2);
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(pvm);
+       }
+
+       private void readPairPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read value format for first glyph
+               int vf1 = in.readTTFUShort();
+               // read value format for second glyph
+               int vf2 = in.readTTFUShort();
+               // read class def 1 offset
+               int cd1o = in.readTTFUShort();
+               // read class def 2 offset
+               int cd2o = in.readTTFUShort();
+               // read number (count) of classes in class def 1 table
+               int nc1 = in.readTTFUShort();
+               // read number (count) of classes in class def 2 table
+               int nc2 = in.readTTFUShort();
+               // read coverage table
+               GlyphCoverageTable ct = readCoverageTable(tableTag + " pair positioning coverage", subtableOffset + co);
+               // read class definition table #1
+               GlyphClassTable cdt1 = readClassDefTable(tableTag + " pair positioning class definition #1", subtableOffset + cd1o);
+               // read class definition table #2
+               GlyphClassTable cdt2 = readClassDefTable(tableTag + " pair positioning class definition #2", subtableOffset + cd2o);
+               // read pair value matrix
+               GlyphPositioningTable.PairValues[][] pvm = new GlyphPositioningTable.PairValues[nc1][nc2];
+               for (int i = 0; i < nc1; i++) {
+                       for (int j = 0; j < nc2; j++) {
+                               GlyphPositioningTable.PairValues pv = readPosPairValues(subtableOffset, false, vf1, vf2);
+                               pvm[i][j] = pv;
+                       }
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(cdt1);
+               seEntries.add(cdt2);
+               seEntries.add(Integer.valueOf(nc1));
+               seEntries.add(Integer.valueOf(nc2));
+               seEntries.add(pvm);
+       }
+
+       private int readPairPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read positioning subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readPairPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 2) {
+                       readPairPosTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported pair positioning subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private GlyphPositioningTable.Anchor readPosAnchor(long anchorTableOffset) throws IOException {
+               GlyphPositioningTable.Anchor a;
+               long cp = in.getCurrentPos();
+               in.seekSet(anchorTableOffset);
+               // read anchor table format
+               int af = in.readTTFUShort();
+               if (af == 1) {
+                       // read x coordinate
+                       int x = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
+                       // read y coordinate
+                       int y = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
+                       a = new GlyphPositioningTable.Anchor(x, y);
+               } else if (af == 2) {
+                       // read x coordinate
+                       int x = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
+                       // read y coordinate
+                       int y = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
+                       // read anchor point index
+                       int ap = in.readTTFUShort();
+                       a = new GlyphPositioningTable.Anchor(x, y, ap);
+               } else if (af == 3) {
+                       // read x coordinate
+                       int x = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
+                       // read y coordinate
+                       int y = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
+                       // read x device table offset
+                       int xdo = in.readTTFUShort();
+                       // read y device table offset
+                       int ydo = in.readTTFUShort();
+                       // read x device table (if present)
+                       GlyphPositioningTable.DeviceTable xd;
+                       if (xdo != 0) {
+                               xd = readPosDeviceTable(cp, xdo);
+                       } else {
+                               xd = null;
+                       }
+                       // read y device table (if present)
+                       GlyphPositioningTable.DeviceTable yd;
+                       if (ydo != 0) {
+                               yd = readPosDeviceTable(cp, ydo);
+                       } else {
+                               yd = null;
+                       }
+                       a = new GlyphPositioningTable.Anchor(x, y, xd, yd);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported positioning anchor format: " + af);
+               }
+               in.seekSet(cp);
+               return a;
+       }
+
+       private void readCursivePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read entry/exit count
+               int ec = in.readTTFUShort();
+               // read coverage table
+               GlyphCoverageTable ct = readCoverageTable(tableTag + " cursive positioning coverage", subtableOffset + co);
+               // read entry/exit records
+               GlyphPositioningTable.Anchor[] aa = new GlyphPositioningTable.Anchor[ec * 2];
+               for (int i = 0, n = ec; i < n; i++) {
+                       // read entry anchor offset
+                       int eno = in.readTTFUShort();
+                       // read exit anchor offset
+                       int exo = in.readTTFUShort();
+                       // read entry anchor
+                       GlyphPositioningTable.Anchor ena;
+                       if (eno > 0) {
+                               ena = readPosAnchor(subtableOffset + eno);
+                       } else {
+                               ena = null;
+                       }
+                       // read exit anchor
+                       GlyphPositioningTable.Anchor exa;
+                       if (exo > 0) {
+                               exa = readPosAnchor(subtableOffset + exo);
+                       } else {
+                               exa = null;
+                       }
+                       aa[(i * 2) + 0] = ena;
+                       aa[(i * 2) + 1] = exa;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(aa);
+       }
+
+       private int readCursivePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read positioning subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readCursivePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported cursive positioning subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readMarkToBasePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read mark coverage offset
+               int mco = in.readTTFUShort();
+               // read base coverage offset
+               int bco = in.readTTFUShort();
+               // read mark class count
+               int nmc = in.readTTFUShort();
+               // read mark array offset
+               int mao = in.readTTFUShort();
+               // read base array offset
+               int bao = in.readTTFUShort();
+               // read mark coverage table
+               GlyphCoverageTable mct =
+                               readCoverageTable(tableTag + " mark-to-base positioning mark coverage", subtableOffset + mco);
+               // read base coverage table
+               GlyphCoverageTable bct =
+                               readCoverageTable(tableTag + " mark-to-base positioning base coverage", subtableOffset + bco);
+               // read mark anchor array
+               // seek to mark array
+               in.seekSet(subtableOffset + mao);
+               // read mark count
+               int nm = in.readTTFUShort();
+               // read mark anchor array, where i:{0...markCount}
+               GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor[nm];
+               for (int i = 0; i < nm; i++) {
+                       // read mark class
+                       int mc = in.readTTFUShort();
+                       // read mark anchor offset
+                       int ao = in.readTTFUShort();
+                       GlyphPositioningTable.Anchor a;
+                       if (ao > 0) {
+                               a = readPosAnchor(subtableOffset + mao + ao);
+                       } else {
+                               a = null;
+                       }
+                       GlyphPositioningTable.MarkAnchor ma;
+                       if (a != null) {
+                               ma = new GlyphPositioningTable.MarkAnchor(mc, a);
+                       } else {
+                               ma = null;
+                       }
+                       maa[i] = ma;
+
+               }
+               // read base anchor matrix
+               // seek to base array
+               in.seekSet(subtableOffset + bao);
+               // read base count
+               int nb = in.readTTFUShort();
+               // read anchor matrix, where i:{0...baseCount - 1}, j:{0...markClassCount - 1}
+               GlyphPositioningTable.Anchor[][] bam = new GlyphPositioningTable.Anchor[nb][nmc];
+               for (int i = 0; i < nb; i++) {
+                       for (int j = 0; j < nmc; j++) {
+                               // read base anchor offset
+                               int ao = in.readTTFUShort();
+                               GlyphPositioningTable.Anchor a;
+                               if (ao > 0) {
+                                       a = readPosAnchor(subtableOffset + bao + ao);
+                               } else {
+                                       a = null;
+                               }
+                               bam[i][j] = a;
+                       }
+               }
+               // store results
+               seMapping = mct;
+               seEntries.add(bct);
+               seEntries.add(Integer.valueOf(nmc));
+               seEntries.add(maa);
+               seEntries.add(bam);
+       }
+
+       private int readMarkToBasePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read positioning subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readMarkToBasePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported mark-to-base positioning subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readMarkToLigaturePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset,
+                                                                                                  int subtableFormat) throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read mark coverage offset
+               int mco = in.readTTFUShort();
+               // read ligature coverage offset
+               int lco = in.readTTFUShort();
+               // read mark class count
+               int nmc = in.readTTFUShort();
+               // read mark array offset
+               int mao = in.readTTFUShort();
+               // read ligature array offset
+               int lao = in.readTTFUShort();
+               // read mark coverage table
+               GlyphCoverageTable mct =
+                               readCoverageTable(tableTag + " mark-to-ligature positioning mark coverage", subtableOffset + mco);
+               // read ligature coverage table
+               GlyphCoverageTable lct =
+                               readCoverageTable(tableTag + " mark-to-ligature positioning ligature coverage", subtableOffset + lco);
+               // read mark anchor array
+               // seek to mark array
+               in.seekSet(subtableOffset + mao);
+               // read mark count
+               int nm = in.readTTFUShort();
+               // read mark anchor array, where i:{0...markCount}
+               GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor[nm];
+               for (int i = 0; i < nm; i++) {
+                       // read mark class
+                       int mc = in.readTTFUShort();
+                       // read mark anchor offset
+                       int ao = in.readTTFUShort();
+                       GlyphPositioningTable.Anchor a;
+                       if (ao > 0) {
+                               a = readPosAnchor(subtableOffset + mao + ao);
+                       } else {
+                               a = null;
+                       }
+                       GlyphPositioningTable.MarkAnchor ma;
+                       if (a != null) {
+                               ma = new GlyphPositioningTable.MarkAnchor(mc, a);
+                       } else {
+                               ma = null;
+                       }
+                       maa[i] = ma;
+               }
+               // read ligature anchor matrix
+               // seek to ligature array
+               in.seekSet(subtableOffset + lao);
+               // read ligature count
+               int nl = in.readTTFUShort();
+               // read ligature attach table offsets
+               int[] laoa = new int[nl];
+               for (int i = 0; i < nl; i++) {
+                       laoa[i] = in.readTTFUShort();
+               }
+               // iterate over ligature attach tables, recording maximum component count
+               int mxc = 0;
+               for (int i = 0; i < nl; i++) {
+                       int lato = laoa[i];
+                       in.seekSet(subtableOffset + lao + lato);
+                       // read component count
+                       int cc = in.readTTFUShort();
+                       if (cc > mxc) {
+                               mxc = cc;
+                       }
+               }
+               // read anchor matrix, where i:{0...ligatureCount - 1}, j:{0...maxComponentCount - 1}, k:{0...markClassCount - 1}
+               GlyphPositioningTable.Anchor[][][] lam = new GlyphPositioningTable.Anchor[nl][][];
+               for (int i = 0; i < nl; i++) {
+                       int lato = laoa[i];
+                       // seek to ligature attach table for ligature[i]
+                       in.seekSet(subtableOffset + lao + lato);
+                       // read component count
+                       int cc = in.readTTFUShort();
+                       GlyphPositioningTable.Anchor[][] lcm = new GlyphPositioningTable.Anchor[cc][nmc];
+                       for (int j = 0; j < cc; j++) {
+                               for (int k = 0; k < nmc; k++) {
+                                       // read ligature anchor offset
+                                       int ao = in.readTTFUShort();
+                                       GlyphPositioningTable.Anchor a;
+                                       if (ao > 0) {
+                                               a = readPosAnchor(subtableOffset + lao + lato + ao);
+                                       } else {
+                                               a = null;
+                                       }
+                                       lcm[j][k] = a;
+                               }
+                       }
+                       lam[i] = lcm;
+               }
+               // store results
+               seMapping = mct;
+               seEntries.add(lct);
+               seEntries.add(Integer.valueOf(nmc));
+               seEntries.add(Integer.valueOf(mxc));
+               seEntries.add(maa);
+               seEntries.add(lam);
+       }
+
+       private int readMarkToLigaturePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read positioning subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readMarkToLigaturePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException(
+                                       "unsupported mark-to-ligature positioning subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readMarkToMarkPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read mark #1 coverage offset
+               int m1co = in.readTTFUShort();
+               // read mark #2 coverage offset
+               int m2co = in.readTTFUShort();
+               // read mark class count
+               int nmc = in.readTTFUShort();
+               // read mark #1 array offset
+               int m1ao = in.readTTFUShort();
+               // read mark #2 array offset
+               int m2ao = in.readTTFUShort();
+               // read mark #1 coverage table
+               GlyphCoverageTable mct1 =
+                               readCoverageTable(tableTag + " mark-to-mark positioning mark #1 coverage", subtableOffset + m1co);
+               // read mark #2 coverage table
+               GlyphCoverageTable mct2 =
+                               readCoverageTable(tableTag + " mark-to-mark positioning mark #2 coverage", subtableOffset + m2co);
+               // read mark #1 anchor array
+               // seek to mark array
+               in.seekSet(subtableOffset + m1ao);
+               // read mark count
+               int nm1 = in.readTTFUShort();
+               // read mark anchor array, where i:{0...mark1Count}
+               GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor[nm1];
+               for (int i = 0; i < nm1; i++) {
+                       // read mark class
+                       int mc = in.readTTFUShort();
+                       // read mark anchor offset
+                       int ao = in.readTTFUShort();
+                       GlyphPositioningTable.Anchor a;
+                       if (ao > 0) {
+                               a = readPosAnchor(subtableOffset + m1ao + ao);
+                       } else {
+                               a = null;
+                       }
+                       GlyphPositioningTable.MarkAnchor ma;
+                       if (a != null) {
+                               ma = new GlyphPositioningTable.MarkAnchor(mc, a);
+                       } else {
+                               ma = null;
+                       }
+                       maa[i] = ma;
+               }
+               // read mark #2 anchor matrix
+               // seek to mark #2 array
+               in.seekSet(subtableOffset + m2ao);
+               // read mark #2 count
+               int nm2 = in.readTTFUShort();
+               // read anchor matrix, where i:{0...mark2Count - 1}, j:{0...markClassCount - 1}
+               GlyphPositioningTable.Anchor[][] mam = new GlyphPositioningTable.Anchor[nm2][nmc];
+               for (int i = 0; i < nm2; i++) {
+                       for (int j = 0; j < nmc; j++) {
+                               // read mark anchor offset
+                               int ao = in.readTTFUShort();
+                               GlyphPositioningTable.Anchor a;
+                               if (ao > 0) {
+                                       a = readPosAnchor(subtableOffset + m2ao + ao);
+                               } else {
+                                       a = null;
+                               }
+                               mam[i][j] = a;
+                       }
+               }
+               // store results
+               seMapping = mct1;
+               seEntries.add(mct2);
+               seEntries.add(Integer.valueOf(nmc));
+               seEntries.add(maa);
+               seEntries.add(mam);
+       }
+
+       private int readMarkToMarkPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read positioning subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readMarkToMarkPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported mark-to-mark positioning subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readContextualPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read rule set count
+               int nrs = in.readTTFUShort();
+               // read rule set offsets
+               int[] rsoa = new int[nrs];
+               for (int i = 0; i < nrs; i++) {
+                       rsoa[i] = in.readTTFUShort();
+               }
+               // read coverage table
+               GlyphCoverageTable ct;
+               if (co > 0) {
+                       ct = readCoverageTable(tableTag + " contextual positioning coverage", subtableOffset + co);
+               } else {
+                       ct = null;
+               }
+               // read rule sets
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[nrs];
+               String header = null;
+               for (int i = 0; i < nrs; i++) {
+                       GlyphTable.RuleSet rs;
+                       int rso = rsoa[i];
+                       if (rso > 0) {
+                               // seek to rule set [ i ]
+                               in.seekSet(subtableOffset + rso);
+                               // read rule count
+                               int nr = in.readTTFUShort();
+                               // read rule offsets
+                               int[] roa = new int[nr];
+                               GlyphTable.Rule[] ra = new GlyphTable.Rule[nr];
+                               for (int j = 0; j < nr; j++) {
+                                       roa[j] = in.readTTFUShort();
+                               }
+                               // read glyph sequence rules
+                               for (int j = 0; j < nr; j++) {
+                                       GlyphTable.GlyphSequenceRule r;
+                                       int ro = roa[j];
+                                       if (ro > 0) {
+                                               // seek to rule [ j ]
+                                               in.seekSet(subtableOffset + rso + ro);
+                                               // read glyph count
+                                               int ng = in.readTTFUShort();
+                                               // read rule lookup count
+                                               int nl = in.readTTFUShort();
+                                               // read glyphs
+                                               int[] glyphs = new int[ng - 1];
+                                               for (int k = 0, nk = glyphs.length; k < nk; k++) {
+                                                       glyphs[k] = in.readTTFUShort();
+                                               }
+                                               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+                                               r = new GlyphTable.GlyphSequenceRule(lookups, ng, glyphs);
+                                       } else {
+                                               r = null;
+                                       }
+                                       ra[j] = r;
+                               }
+                               rs = new GlyphTable.HomogeneousRuleSet(ra);
+                       } else {
+                               rs = null;
+                       }
+                       rsa[i] = rs;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(rsa);
+       }
+
+       private void readContextualPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read class def table offset
+               int cdo = in.readTTFUShort();
+               // read class rule set count
+               int ngc = in.readTTFUShort();
+               // read class rule set offsets
+               int[] csoa = new int[ngc];
+               for (int i = 0; i < ngc; i++) {
+                       csoa[i] = in.readTTFUShort();
+               }
+               // read coverage table
+               GlyphCoverageTable ct;
+               if (co > 0) {
+                       ct = readCoverageTable(tableTag + " contextual positioning coverage", subtableOffset + co);
+               } else {
+                       ct = null;
+               }
+               // read class definition table
+               GlyphClassTable cdt;
+               if (cdo > 0) {
+                       cdt = readClassDefTable(tableTag + " contextual positioning class definition", subtableOffset + cdo);
+               } else {
+                       cdt = null;
+               }
+               // read rule sets
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[ngc];
+               String header = null;
+               for (int i = 0; i < ngc; i++) {
+                       int cso = csoa[i];
+                       GlyphTable.RuleSet rs;
+                       if (cso > 0) {
+                               // seek to rule set [ i ]
+                               in.seekSet(subtableOffset + cso);
+                               // read rule count
+                               int nr = in.readTTFUShort();
+                               // read rule offsets
+                               int[] roa = new int[nr];
+                               GlyphTable.Rule[] ra = new GlyphTable.Rule[nr];
+                               for (int j = 0; j < nr; j++) {
+                                       roa[j] = in.readTTFUShort();
+                               }
+                               // read glyph sequence rules
+                               for (int j = 0; j < nr; j++) {
+                                       int ro = roa[j];
+                                       GlyphTable.ClassSequenceRule r;
+                                       if (ro > 0) {
+                                               // seek to rule [ j ]
+                                               in.seekSet(subtableOffset + cso + ro);
+                                               // read glyph count
+                                               int ng = in.readTTFUShort();
+                                               // read rule lookup count
+                                               int nl = in.readTTFUShort();
+                                               // read classes
+                                               int[] classes = new int[ng - 1];
+                                               for (int k = 0, nk = classes.length; k < nk; k++) {
+                                                       classes[k] = in.readTTFUShort();
+                                               }
+                                               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+                                               r = new GlyphTable.ClassSequenceRule(lookups, ng, classes);
+                                       } else {
+                                               r = null;
+                                       }
+                                       ra[j] = r;
+                               }
+                               rs = new GlyphTable.HomogeneousRuleSet(ra);
+                       } else {
+                               rs = null;
+                       }
+                       rsa[i] = rs;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(cdt);
+               seEntries.add(Integer.valueOf(ngc));
+               seEntries.add(rsa);
+       }
+
+       private void readContextualPosTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat)
+                       throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read glyph (input sequence length) count
+               int ng = in.readTTFUShort();
+               // read positioning lookup count
+               int nl = in.readTTFUShort();
+               // read glyph coverage offsets, one per glyph input sequence length count
+               int[] gcoa = new int[ng];
+               for (int i = 0; i < ng; i++) {
+                       gcoa[i] = in.readTTFUShort();
+               }
+               // read coverage tables
+               GlyphCoverageTable[] gca = new GlyphCoverageTable[ng];
+               for (int i = 0; i < ng; i++) {
+                       int gco = gcoa[i];
+                       GlyphCoverageTable gct;
+                       if (gco > 0) {
+                               gct = readCoverageTable(tableTag + " contextual positioning coverage[" + i + "]", subtableOffset + gcoa[i]);
+                       } else {
+                               gct = null;
+                       }
+                       gca[i] = gct;
+               }
+               // read rule lookups
+               String header = null;
+               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+               // construct rule, rule set, and rule set array
+               GlyphTable.Rule r = new GlyphTable.CoverageSequenceRule(lookups, ng, gca);
+               GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet(new GlyphTable.Rule[]{r});
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[]{rs};
+               // store results
+               assert (gca != null) && (gca.length > 0);
+               seMapping = gca[0];
+               seEntries.add(rsa);
+       }
+
+       private int readContextualPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read positioning subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readContextualPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 2) {
+                       readContextualPosTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 3) {
+                       readContextualPosTableFormat3(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported contextual positioning subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readChainedContextualPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset,
+                                                                                                         int subtableFormat) throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read rule set count
+               int nrs = in.readTTFUShort();
+               // read rule set offsets
+               int[] rsoa = new int[nrs];
+               for (int i = 0; i < nrs; i++) {
+                       rsoa[i] = in.readTTFUShort();
+               }
+               // read coverage table
+               GlyphCoverageTable ct;
+               if (co > 0) {
+                       ct = readCoverageTable(tableTag + " chained contextual positioning coverage", subtableOffset + co);
+               } else {
+                       ct = null;
+               }
+               // read rule sets
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[nrs];
+               String header = null;
+               for (int i = 0; i < nrs; i++) {
+                       GlyphTable.RuleSet rs;
+                       int rso = rsoa[i];
+                       if (rso > 0) {
+                               // seek to rule set [ i ]
+                               in.seekSet(subtableOffset + rso);
+                               // read rule count
+                               int nr = in.readTTFUShort();
+                               // read rule offsets
+                               int[] roa = new int[nr];
+                               GlyphTable.Rule[] ra = new GlyphTable.Rule[nr];
+                               for (int j = 0; j < nr; j++) {
+                                       roa[j] = in.readTTFUShort();
+                               }
+                               // read glyph sequence rules
+                               for (int j = 0; j < nr; j++) {
+                                       GlyphTable.ChainedGlyphSequenceRule r;
+                                       int ro = roa[j];
+                                       if (ro > 0) {
+                                               // seek to rule [ j ]
+                                               in.seekSet(subtableOffset + rso + ro);
+                                               // read backtrack glyph count
+                                               int nbg = in.readTTFUShort();
+                                               // read backtrack glyphs
+                                               int[] backtrackGlyphs = new int[nbg];
+                                               for (int k = 0, nk = backtrackGlyphs.length; k < nk; k++) {
+                                                       backtrackGlyphs[k] = in.readTTFUShort();
+                                               }
+                                               // read input glyph count
+                                               int nig = in.readTTFUShort();
+                                               // read glyphs
+                                               int[] glyphs = new int[nig - 1];
+                                               for (int k = 0, nk = glyphs.length; k < nk; k++) {
+                                                       glyphs[k] = in.readTTFUShort();
+                                               }
+                                               // read lookahead glyph count
+                                               int nlg = in.readTTFUShort();
+                                               // read lookahead glyphs
+                                               int[] lookaheadGlyphs = new int[nlg];
+                                               for (int k = 0, nk = lookaheadGlyphs.length; k < nk; k++) {
+                                                       lookaheadGlyphs[k] = in.readTTFUShort();
+                                               }
+                                               // read rule lookup count
+                                               int nl = in.readTTFUShort();
+                                               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+                                               r = new GlyphTable.ChainedGlyphSequenceRule(lookups, nig, glyphs, backtrackGlyphs, lookaheadGlyphs);
+                                       } else {
+                                               r = null;
+                                       }
+                                       ra[j] = r;
+                               }
+                               rs = new GlyphTable.HomogeneousRuleSet(ra);
+                       } else {
+                               rs = null;
+                       }
+                       rsa[i] = rs;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(rsa);
+       }
+
+       private void readChainedContextualPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset,
+                                                                                                         int subtableFormat) throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read backtrack class def table offset
+               int bcdo = in.readTTFUShort();
+               // read input class def table offset
+               int icdo = in.readTTFUShort();
+               // read lookahead class def table offset
+               int lcdo = in.readTTFUShort();
+               // read class set count
+               int ngc = in.readTTFUShort();
+               // read class set offsets
+               int[] csoa = new int[ngc];
+               for (int i = 0; i < ngc; i++) {
+                       csoa[i] = in.readTTFUShort();
+               }
+               // read coverage table
+               GlyphCoverageTable ct;
+               if (co > 0) {
+                       ct = readCoverageTable(tableTag + " chained contextual positioning coverage", subtableOffset + co);
+               } else {
+                       ct = null;
+               }
+               // read backtrack class definition table
+               GlyphClassTable bcdt;
+               if (bcdo > 0) {
+                       bcdt = readClassDefTable(tableTag + " contextual positioning backtrack class definition", subtableOffset + bcdo);
+               } else {
+                       bcdt = null;
+               }
+               // read input class definition table
+               GlyphClassTable icdt;
+               if (icdo > 0) {
+                       icdt = readClassDefTable(tableTag + " contextual positioning input class definition", subtableOffset + icdo);
+               } else {
+                       icdt = null;
+               }
+               // read lookahead class definition table
+               GlyphClassTable lcdt;
+               if (lcdo > 0) {
+                       lcdt = readClassDefTable(tableTag + " contextual positioning lookahead class definition", subtableOffset + lcdo);
+               } else {
+                       lcdt = null;
+               }
+               // read rule sets
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[ngc];
+               String header = null;
+               for (int i = 0; i < ngc; i++) {
+                       int cso = csoa[i];
+                       GlyphTable.RuleSet rs;
+                       if (cso > 0) {
+                               // seek to rule set [ i ]
+                               in.seekSet(subtableOffset + cso);
+                               // read rule count
+                               int nr = in.readTTFUShort();
+                               // read rule offsets
+                               int[] roa = new int[nr];
+                               GlyphTable.Rule[] ra = new GlyphTable.Rule[nr];
+                               for (int j = 0; j < nr; j++) {
+                                       roa[j] = in.readTTFUShort();
+                               }
+                               // read glyph sequence rules
+                               for (int j = 0; j < nr; j++) {
+                                       GlyphTable.ChainedClassSequenceRule r;
+                                       int ro = roa[j];
+                                       if (ro > 0) {
+                                               // seek to rule [ j ]
+                                               in.seekSet(subtableOffset + cso + ro);
+                                               // read backtrack glyph class count
+                                               int nbc = in.readTTFUShort();
+                                               // read backtrack glyph classes
+                                               int[] backtrackClasses = new int[nbc];
+                                               for (int k = 0, nk = backtrackClasses.length; k < nk; k++) {
+                                                       backtrackClasses[k] = in.readTTFUShort();
+                                               }
+                                               // read input glyph class count
+                                               int nic = in.readTTFUShort();
+                                               // read input glyph classes
+                                               int[] classes = new int[nic - 1];
+                                               for (int k = 0, nk = classes.length; k < nk; k++) {
+                                                       classes[k] = in.readTTFUShort();
+                                               }
+                                               // read lookahead glyph class count
+                                               int nlc = in.readTTFUShort();
+                                               // read lookahead glyph classes
+                                               int[] lookaheadClasses = new int[nlc];
+                                               for (int k = 0, nk = lookaheadClasses.length; k < nk; k++) {
+                                                       lookaheadClasses[k] = in.readTTFUShort();
+                                               }
+                                               // read rule lookup count
+                                               int nl = in.readTTFUShort();
+                                               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+                                               r = new GlyphTable.ChainedClassSequenceRule(lookups, nic, classes, backtrackClasses, lookaheadClasses);
+                                       } else {
+                                               r = null;
+                                       }
+                                       ra[j] = r;
+                               }
+                               rs = new GlyphTable.HomogeneousRuleSet(ra);
+                       } else {
+                               rs = null;
+                       }
+                       rsa[i] = rs;
+               }
+               // store results
+               seMapping = ct;
+               seEntries.add(icdt);
+               seEntries.add(bcdt);
+               seEntries.add(lcdt);
+               seEntries.add(Integer.valueOf(ngc));
+               seEntries.add(rsa);
+       }
+
+       private void readChainedContextualPosTableFormat3(int lookupType, int lookupFlags, long subtableOffset,
+                                                                                                         int subtableFormat) throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read backtrack glyph count
+               int nbg = in.readTTFUShort();
+               // read backtrack glyph coverage offsets
+               int[] bgcoa = new int[nbg];
+               for (int i = 0; i < nbg; i++) {
+                       bgcoa[i] = in.readTTFUShort();
+               }
+               // read input glyph count
+               int nig = in.readTTFUShort();
+               // read backtrack glyph coverage offsets
+               int[] igcoa = new int[nig];
+               for (int i = 0; i < nig; i++) {
+                       igcoa[i] = in.readTTFUShort();
+               }
+               // read lookahead glyph count
+               int nlg = in.readTTFUShort();
+               // read backtrack glyph coverage offsets
+               int[] lgcoa = new int[nlg];
+               for (int i = 0; i < nlg; i++) {
+                       lgcoa[i] = in.readTTFUShort();
+               }
+               // read positioning lookup count
+               int nl = in.readTTFUShort();
+               // read backtrack coverage tables
+               GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg];
+               for (int i = 0; i < nbg; i++) {
+                       int bgco = bgcoa[i];
+                       GlyphCoverageTable bgct;
+                       if (bgco > 0) {
+                               bgct = readCoverageTable(tableTag + " chained contextual positioning backtrack coverage[" + i + "]",
+                                               subtableOffset + bgco);
+                       } else {
+                               bgct = null;
+                       }
+                       bgca[i] = bgct;
+               }
+               // read input coverage tables
+               GlyphCoverageTable[] igca = new GlyphCoverageTable[nig];
+               for (int i = 0; i < nig; i++) {
+                       int igco = igcoa[i];
+                       GlyphCoverageTable igct;
+                       if (igco > 0) {
+                               igct = readCoverageTable(tableTag + " chained contextual positioning input coverage[" + i + "]",
+                                               subtableOffset + igco);
+                       } else {
+                               igct = null;
+                       }
+                       igca[i] = igct;
+               }
+               // read lookahead coverage tables
+               GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg];
+               for (int i = 0; i < nlg; i++) {
+                       int lgco = lgcoa[i];
+                       GlyphCoverageTable lgct;
+                       if (lgco > 0) {
+                               lgct = readCoverageTable(tableTag + " chained contextual positioning lookahead coverage[" + i + "]",
+                                               subtableOffset + lgco);
+                       } else {
+                               lgct = null;
+                       }
+                       lgca[i] = lgct;
+               }
+               // read rule lookups
+               String header = null;
+               GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
+               // construct rule, rule set, and rule set array
+               GlyphTable.Rule r = new GlyphTable.ChainedCoverageSequenceRule(lookups, nig, igca, bgca, lgca);
+               GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet(new GlyphTable.Rule[]{r});
+               GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[]{rs};
+               // store results
+               assert (igca != null) && (igca.length > 0);
+               seMapping = igca[0];
+               seEntries.add(rsa);
+       }
+
+       private int readChainedContextualPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read positioning subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readChainedContextualPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 2) {
+                       readChainedContextualPosTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
+               } else if (sf == 3) {
+                       readChainedContextualPosTableFormat3(lookupType, lookupFlags, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException(
+                                       "unsupported chained contextual positioning subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readExtensionPosTableFormat1(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence,
+                                                                                         long subtableOffset, int subtableFormat) throws IOException {
+               String tableTag = "GPOS";
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read extension lookup type
+               int lt = in.readTTFUShort();
+               // read extension offset
+               long eo = in.readTTFULong();
+               // read referenced subtable from extended offset
+               readGPOSSubtable(lt, lookupFlags, lookupSequence, subtableSequence, subtableOffset + eo);
+       }
+
+       private int readExtensionPosTable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence,
+                                                                         long subtableOffset) throws IOException {
+               in.seekSet(subtableOffset);
+               // read positioning subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readExtensionPosTableFormat1(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported extension positioning subtable format: " + sf);
+               }
+               return sf;
+       }
+
+       private void readGPOSSubtable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence,
+                                                                 long subtableOffset) throws IOException {
+               initATSubState();
+               int subtableFormat = -1;
+               switch (lookupType) {
+                       case GPOSLookupType.SINGLE:
+                               subtableFormat = readSinglePosTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GPOSLookupType.PAIR:
+                               subtableFormat = readPairPosTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GPOSLookupType.CURSIVE:
+                               subtableFormat = readCursivePosTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GPOSLookupType.MARK_TO_BASE:
+                               subtableFormat = readMarkToBasePosTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GPOSLookupType.MARK_TO_LIGATURE:
+                               subtableFormat = readMarkToLigaturePosTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GPOSLookupType.MARK_TO_MARK:
+                               subtableFormat = readMarkToMarkPosTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GPOSLookupType.CONTEXTUAL:
+                               subtableFormat = readContextualPosTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GPOSLookupType.CHAINED_CONTEXTUAL:
+                               subtableFormat = readChainedContextualPosTable(lookupType, lookupFlags, subtableOffset);
+                               break;
+                       case GPOSLookupType.EXTENSION:
+                               subtableFormat =
+                                               readExtensionPosTable(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset);
+                               break;
+                       default:
+                               break;
+               }
+               extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_POSITIONING, lookupType, lookupFlags, lookupSequence,
+                               subtableSequence, subtableFormat);
+               resetATSubState();
+       }
+
+       private void readLookupTable(OFTableName tableTag, int lookupSequence, long lookupTable) throws IOException {
+               boolean isGSUB = tableTag.equals(OFTableName.GSUB);
+               boolean isGPOS = tableTag.equals(OFTableName.GPOS);
+               in.seekSet(lookupTable);
+               // read lookup type
+               int lt = in.readTTFUShort();
+               // read lookup flags
+               int lf = in.readTTFUShort();
+               // read sub-table count
+               int ns = in.readTTFUShort();
+               // read subtable offsets
+               int[] soa = new int[ns];
+               for (int i = 0; i < ns; i++) {
+                       int so = in.readTTFUShort();
+                       soa[i] = so;
+               }
+               // read mark filtering set
+               if ((lf & LookupFlag.USE_MARK_FILTERING_SET) != 0) {
+                       // read mark filtering set
+                       int fs = in.readTTFUShort();
+               }
+               // read subtables
+               for (int i = 0; i < ns; i++) {
+                       int so = soa[i];
+                       if (isGSUB) {
+                               readGSUBSubtable(lt, lf, lookupSequence, i, lookupTable + so);
+                       } else if (isGPOS) {
+                               readGPOSSubtable(lt, lf, lookupSequence, i, lookupTable + so);
+                       }
+               }
+       }
+
+       private void readLookupList(OFTableName tableTag, long lookupList) throws IOException {
+               in.seekSet(lookupList);
+               // read lookup record count
+               int nl = in.readTTFUShort();
+               if (nl > 0) {
+                       int[] loa = new int[nl];
+                       // read lookup records
+                       for (int i = 0, n = nl; i < n; i++) {
+                               int lo = in.readTTFUShort();
+                               loa[i] = lo;
+                       }
+                       // read lookup tables
+                       for (int i = 0, n = nl; i < n; i++) {
+                               readLookupTable(tableTag, i, lookupList + loa[i]);
+                       }
+               }
+       }
+
+       /**
+        * Read the common layout tables (used by GSUB and GPOS).
+        *
+        * @param tableTag    tag of table being read
+        * @param scriptList  offset to script list from beginning of font file
+        * @param featureList offset to feature list from beginning of font file
+        * @param lookupList  offset to lookup list from beginning of font file
+        * @throws IOException In case of a I/O problem
+        */
+       private void readCommonLayoutTables(OFTableName tableTag, long scriptList, long featureList, long lookupList)
+                       throws IOException {
+               if (scriptList > 0) {
+                       readScriptList(tableTag, scriptList);
+               }
+               if (featureList > 0) {
+                       readFeatureList(tableTag, featureList);
+               }
+               if (lookupList > 0) {
+                       readLookupList(tableTag, lookupList);
+               }
+       }
+
+       private void readGDEFClassDefTable(OFTableName tableTag, int lookupSequence, long subtableOffset) throws IOException {
+               initATSubState();
+               in.seekSet(subtableOffset);
+               // subtable is a bare class definition table
+               GlyphClassTable ct = readClassDefTable(tableTag + " glyph class definition table", subtableOffset);
+               // store results
+               seMapping = ct;
+               // extract subtable
+               extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.GLYPH_CLASS, 0, lookupSequence, 0, 1);
+               resetATSubState();
+       }
+
+       private void readGDEFAttachmentTable(OFTableName tableTag, int lookupSequence, long subtableOffset)
+                       throws IOException {
+               initATSubState();
+               in.seekSet(subtableOffset);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read coverage table
+               GlyphCoverageTable ct = readCoverageTable(tableTag + " attachment point coverage", subtableOffset + co);
+               // store results
+               seMapping = ct;
+               // extract subtable
+               extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.ATTACHMENT_POINT, 0, lookupSequence, 0, 1);
+               resetATSubState();
+       }
+
+       private void readGDEFLigatureCaretTable(OFTableName tableTag, int lookupSequence, long subtableOffset)
+                       throws IOException {
+               initATSubState();
+               in.seekSet(subtableOffset);
+               // read coverage offset
+               int co = in.readTTFUShort();
+               // read ligature glyph count
+               int nl = in.readTTFUShort();
+               // read ligature glyph table offsets
+               int[] lgto = new int[nl];
+               for (int i = 0; i < nl; i++) {
+                       lgto[i] = in.readTTFUShort();
+               }
+               // read coverage table
+               GlyphCoverageTable ct = readCoverageTable(tableTag + " ligature caret coverage", subtableOffset + co);
+               // store results
+               seMapping = ct;
+               // extract subtable
+               extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.LIGATURE_CARET, 0, lookupSequence, 0, 1);
+               resetATSubState();
+       }
+
+       private void readGDEFMarkAttachmentTable(OFTableName tableTag, int lookupSequence, long subtableOffset)
+                       throws IOException {
+               initATSubState();
+               in.seekSet(subtableOffset);
+               // subtable is a bare class definition table
+               GlyphClassTable ct = readClassDefTable(tableTag + " glyph class definition table", subtableOffset);
+               // store results
+               seMapping = ct;
+               // extract subtable
+               extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.MARK_ATTACHMENT, 0, lookupSequence, 0, 1);
+               resetATSubState();
+       }
+
+       private void readGDEFMarkGlyphsTableFormat1(OFTableName tableTag, int lookupSequence, long subtableOffset,
+                                                                                               int subtableFormat) throws IOException {
+               initATSubState();
+               in.seekSet(subtableOffset);
+               // skip over format (already known)
+               in.skip(2);
+               // read mark set class count
+               int nmc = in.readTTFUShort();
+               long[] mso = new long[nmc];
+               // read mark set coverage offsets
+               for (int i = 0; i < nmc; i++) {
+                       mso[i] = in.readTTFULong();
+               }
+               // read mark set coverage tables, one per class
+               GlyphCoverageTable[] msca = new GlyphCoverageTable[nmc];
+               for (int i = 0; i < nmc; i++) {
+                       msca[i] = readCoverageTable(tableTag + " mark set coverage[" + i + "]", subtableOffset + mso[i]);
+               }
+               // create combined class table from per-class coverage tables
+               GlyphClassTable ct = GlyphClassTable.createClassTable(Arrays.asList(msca));
+               // store results
+               seMapping = ct;
+               // extract subtable
+               extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.MARK_ATTACHMENT, 0, lookupSequence, 0, 1);
+               resetATSubState();
+       }
+
+       private void readGDEFMarkGlyphsTable(OFTableName tableTag, int lookupSequence, long subtableOffset)
+                       throws IOException {
+               in.seekSet(subtableOffset);
+               // read mark set subtable format
+               int sf = in.readTTFUShort();
+               if (sf == 1) {
+                       readGDEFMarkGlyphsTableFormat1(tableTag, lookupSequence, subtableOffset, sf);
+               } else {
+                       throw new AdvancedTypographicTableFormatException("unsupported mark glyph sets subtable format: " + sf);
+               }
+       }
+
+       /**
+        * Read the GDEF table.
+        *
+        * @throws IOException In case of a I/O problem
+        */
+       private void readGDEF() throws IOException {
+               OFTableName tableTag = OFTableName.GDEF;
+               // Initialize temporary state
+               initATState();
+               // Read glyph definition (GDEF) table
+               OFDirTabEntry dirTab = otf.getDirectoryEntry(tableTag);
+               if (gdef != null) {
+
+               } else if (dirTab != null) {
+                       otf.seekTab(in, tableTag, 0);
+                       long version = in.readTTFULong();
+
+                       // glyph class definition table offset (may be null)
+                       int cdo = in.readTTFUShort();
+                       // attach point list offset (may be null)
+                       int apo = in.readTTFUShort();
+                       // ligature caret list offset (may be null)
+                       int lco = in.readTTFUShort();
+                       // mark attach class definition table offset (may be null)
+                       int mao = in.readTTFUShort();
+                       // mark glyph sets definition table offset (may be null)
+                       int mgo;
+                       if (version >= 0x00010002) {
+                               mgo = in.readTTFUShort();
+                       } else {
+                               mgo = 0;
+                       }
+
+                       // initialize subtable sequence number
+                       int seqno = 0;
+                       // obtain offset to start of gdef table
+                       long to = dirTab.getOffset();
+                       // (optionally) read glyph class definition subtable
+                       if (cdo != 0) {
+                               readGDEFClassDefTable(tableTag, seqno++, to + cdo);
+                       }
+                       // (optionally) read glyph attachment point subtable
+                       if (apo != 0) {
+                               readGDEFAttachmentTable(tableTag, seqno++, to + apo);
+                       }
+                       // (optionally) read ligature caret subtable
+                       if (lco != 0) {
+                               readGDEFLigatureCaretTable(tableTag, seqno++, to + lco);
+                       }
+                       // (optionally) read mark attachment class subtable
+                       if (mao != 0) {
+                               readGDEFMarkAttachmentTable(tableTag, seqno++, to + mao);
+                       }
+                       // (optionally) read mark glyph sets subtable
+                       if (mgo != 0) {
+                               readGDEFMarkGlyphsTable(tableTag, seqno++, to + mgo);
+                       }
+                       GlyphDefinitionTable gdef;
+                       if ((gdef = constructGDEF()) != null) {
+                               this.gdef = gdef;
+                       }
+               }
+       }
+
+       /**
+        * Read the GSUB table.
+        *
+        * @throws IOException In case of a I/O problem
+        */
+       private void readGSUB() throws IOException {
+               OFTableName tableTag = OFTableName.GSUB;
+               // Initialize temporary state
+               initATState();
+               // Read glyph substitution (GSUB) table
+               OFDirTabEntry dirTab = otf.getDirectoryEntry(tableTag);
+               if (gpos != null) {
+
+               } else if (dirTab != null) {
+                       otf.seekTab(in, tableTag, 0);
+                       int version = in.readTTFLong();
+
+                       int slo = in.readTTFUShort();
+                       int flo = in.readTTFUShort();
+                       int llo = in.readTTFUShort();
+
+                       long to = dirTab.getOffset();
+                       readCommonLayoutTables(tableTag, to + slo, to + flo, to + llo);
+                       GlyphSubstitutionTable gsub;
+                       if ((gsub = constructGSUB()) != null) {
+                               this.gsub = gsub;
+                       }
+               }
+       }
+
+       /**
+        * Read the GPOS table.
+        *
+        * @throws IOException In case of a I/O problem
+        */
+       private void readGPOS() throws IOException {
+               OFTableName tableTag = OFTableName.GPOS;
+               // Initialize temporary state
+               initATState();
+               // Read glyph positioning (GPOS) table
+               OFDirTabEntry dirTab = otf.getDirectoryEntry(tableTag);
+               if (gpos != null) {
+
+               } else if (dirTab != null) {
+                       otf.seekTab(in, tableTag, 0);
+                       int version = in.readTTFLong();
+
+                       int slo = in.readTTFUShort();
+                       int flo = in.readTTFUShort();
+                       int llo = in.readTTFUShort();
+
+                       long to = dirTab.getOffset();
+                       readCommonLayoutTables(tableTag, to + slo, to + flo, to + llo);
+                       GlyphPositioningTable gpos;
+                       if ((gpos = constructGPOS()) != null) {
+                               this.gpos = gpos;
+                       }
+               }
+       }
+
+       /**
+        * Construct the (internal representation of the) GDEF table based on previously
+        * parsed state.
+        *
+        * @returns glyph definition table or null if insufficient or invalid state
+        */
+       private GlyphDefinitionTable constructGDEF() {
+               GlyphDefinitionTable gdef = null;
+               List subtables;
+               if ((subtables = constructGDEFSubtables()) != null) {
+                       if (subtables.size() > 0) {
+                               gdef = new GlyphDefinitionTable(subtables);
+                       }
+               }
+               resetATState();
+               return gdef;
+       }
+
+       /**
+        * Construct the (internal representation of the) GSUB table based on previously
+        * parsed state.
+        *
+        * @returns glyph substitution table or null if insufficient or invalid state
+        */
+       private GlyphSubstitutionTable constructGSUB() {
+               GlyphSubstitutionTable gsub = null;
+               Map lookups;
+               if ((lookups = constructLookups()) != null) {
+                       List subtables;
+                       if ((subtables = constructGSUBSubtables()) != null) {
+                               if ((lookups.size() > 0) && (subtables.size() > 0)) {
+                                       gsub = new GlyphSubstitutionTable(gdef, lookups, subtables);
+                               }
+                       }
+               }
+               resetATState();
+               return gsub;
+       }
+
+       /**
+        * Construct the (internal representation of the) GPOS table based on previously
+        * parsed state.
+        *
+        * @returns glyph positioning table or null if insufficient or invalid state
+        */
+       private GlyphPositioningTable constructGPOS() {
+               GlyphPositioningTable gpos = null;
+               Map lookups;
+               if ((lookups = constructLookups()) != null) {
+                       List subtables;
+                       if ((subtables = constructGPOSSubtables()) != null) {
+                               if ((lookups.size() > 0) && (subtables.size() > 0)) {
+                                       gpos = new GlyphPositioningTable(gdef, lookups, subtables);
+                               }
+                       }
+               }
+               resetATState();
+               return gpos;
+       }
+
+       private void constructLookupsFeature(Map lookups, String st, String lt, String fid) {
+               Object[] fp = (Object[]) seFeatures.get(fid);
+               if (fp != null) {
+                       assert fp.length == 2;
+                       String ft = (String) fp[0];                 // feature tag
+                       List/*<String>*/ lul = (List) fp[1];        // list of lookup table ids
+                       if ((ft != null) && (lul != null) && (lul.size() > 0)) {
+                               GlyphTable.LookupSpec ls = new GlyphTable.LookupSpec(st, lt, ft);
+                               lookups.put(ls, lul);
+                       }
+               }
+       }
+
+       private void constructLookupsFeatures(Map lookups, String st, String lt, List/*<String>*/ fids) {
+               for (Iterator fit = fids.iterator(); fit.hasNext(); ) {
+                       String fid = (String) fit.next();
+                       constructLookupsFeature(lookups, st, lt, fid);
+               }
+       }
+
+       private void constructLookupsLanguage(Map lookups, String st, String lt, Map/*<String,Object[2]>*/ languages) {
+               Object[] lp = (Object[]) languages.get(lt);
+               if (lp != null) {
+                       assert lp.length == 2;
+                       if (lp[0] != null) {                      // required feature id
+                               constructLookupsFeature(lookups, st, lt, (String) lp[0]);
+                       }
+                       if (lp[1] != null) {                      // non-required features ids
+                               constructLookupsFeatures(lookups, st, lt, (List) lp[1]);
+                       }
+               }
+       }
+
+       private void constructLookupsLanguages(Map lookups, String st, List/*<String>*/ ll,
+                                                                                  Map/*<String,Object[2]>*/ languages) {
+               for (Iterator lit = ll.iterator(); lit.hasNext(); ) {
+                       String lt = (String) lit.next();
+                       constructLookupsLanguage(lookups, st, lt, languages);
+               }
+       }
+
+       private Map constructLookups() {
+               Map/*<GlyphTable.LookupSpec,List<String>>*/ lookups = new java.util.LinkedHashMap();
+               for (Iterator sit = seScripts.keySet().iterator(); sit.hasNext(); ) {
+                       String st = (String) sit.next();
+                       Object[] sp = (Object[]) seScripts.get(st);
+                       if (sp != null) {
+                               assert sp.length == 3;
+                               Map/*<String,Object[2]>*/ languages = (Map) sp[2];
+                               if (sp[0] != null) {                  // default language
+                                       constructLookupsLanguage(lookups, st, (String) sp[0], languages);
+                               }
+                               if (sp[1] != null) {                  // non-default languages
+                                       constructLookupsLanguages(lookups, st, (List) sp[1], languages);
+                               }
+                       }
+               }
+               return lookups;
+       }
+
+       private List constructGDEFSubtables() {
+               List/*<GlyphDefinitionSubtable>*/ subtables = new java.util.ArrayList();
+               if (seSubtables != null) {
+                       for (Iterator it = seSubtables.iterator(); it.hasNext(); ) {
+                               Object[] stp = (Object[]) it.next();
+                               GlyphSubtable st;
+                               if ((st = constructGDEFSubtable(stp)) != null) {
+                                       subtables.add(st);
+                               }
+                       }
+               }
+               return subtables;
+       }
+
+       private GlyphSubtable constructGDEFSubtable(Object[] stp) {
+               GlyphSubtable st = null;
+               assert (stp != null) && (stp.length == 8);
+               Integer tt = (Integer) stp[0];          // table type
+               Integer lt = (Integer) stp[1];          // lookup type
+               Integer ln = (Integer) stp[2];          // lookup sequence number
+               Integer lf = (Integer) stp[3];          // lookup flags
+               Integer sn = (Integer) stp[4];          // subtable sequence number
+               Integer sf = (Integer) stp[5];          // subtable format
+               GlyphMappingTable mapping = (GlyphMappingTable) stp[6];
+               List entries = (List) stp[7];
+               if (tt.intValue() == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION) {
+                       int type = GDEFLookupType.getSubtableType(lt.intValue());
+                       String lid = "lu" + ln.intValue();
+                       int sequence = sn.intValue();
+                       int flags = lf.intValue();
+                       int format = sf.intValue();
+                       st = GlyphDefinitionTable.createSubtable(type, lid, sequence, flags, format, mapping, entries);
+               }
+               return st;
+       }
+
+       private List constructGSUBSubtables() {
+               List/*<GlyphSubtable>*/ subtables = new java.util.ArrayList();
+               if (seSubtables != null) {
+                       for (Iterator it = seSubtables.iterator(); it.hasNext(); ) {
+                               Object[] stp = (Object[]) it.next();
+                               GlyphSubtable st;
+                               if ((st = constructGSUBSubtable(stp)) != null) {
+                                       subtables.add(st);
+                               }
+                       }
+               }
+               return subtables;
+       }
+
+       private GlyphSubtable constructGSUBSubtable(Object[] stp) {
+               GlyphSubtable st = null;
+               assert (stp != null) && (stp.length == 8);
+               Integer tt = (Integer) stp[0];          // table type
+               Integer lt = (Integer) stp[1];          // lookup type
+               Integer ln = (Integer) stp[2];          // lookup sequence number
+               Integer lf = (Integer) stp[3];          // lookup flags
+               Integer sn = (Integer) stp[4];          // subtable sequence number
+               Integer sf = (Integer) stp[5];          // subtable format
+               GlyphCoverageTable coverage = (GlyphCoverageTable) stp[6];
+               List entries = (List) stp[7];
+               if (tt.intValue() == GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION) {
+                       int type = GSUBLookupType.getSubtableType(lt.intValue());
+                       String lid = "lu" + ln.intValue();
+                       int sequence = sn.intValue();
+                       int flags = lf.intValue();
+                       int format = sf.intValue();
+                       st = GlyphSubstitutionTable.createSubtable(type, lid, sequence, flags, format, coverage, entries);
+               }
+               return st;
+       }
+
+       private List constructGPOSSubtables() {
+               List/*<GlyphSubtable>*/ subtables = new java.util.ArrayList();
+               if (seSubtables != null) {
+                       for (Iterator it = seSubtables.iterator(); it.hasNext(); ) {
+                               Object[] stp = (Object[]) it.next();
+                               GlyphSubtable st;
+                               if ((st = constructGPOSSubtable(stp)) != null) {
+                                       subtables.add(st);
+                               }
+                       }
+               }
+               return subtables;
+       }
+
+       private GlyphSubtable constructGPOSSubtable(Object[] stp) {
+               GlyphSubtable st = null;
+               assert (stp != null) && (stp.length == 8);
+               Integer tt = (Integer) stp[0];          // table type
+               Integer lt = (Integer) stp[1];          // lookup type
+               Integer ln = (Integer) stp[2];          // lookup sequence number
+               Integer lf = (Integer) stp[3];          // lookup flags
+               Integer sn = (Integer) stp[4];          // subtable sequence number
+               Integer sf = (Integer) stp[5];          // subtable format
+               GlyphCoverageTable coverage = (GlyphCoverageTable) stp[6];
+               List entries = (List) stp[7];
+               if (tt.intValue() == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING) {
+                       int type = GSUBLookupType.getSubtableType(lt.intValue());
+                       String lid = "lu" + ln.intValue();
+                       int sequence = sn.intValue();
+                       int flags = lf.intValue();
+                       int format = sf.intValue();
+                       st = GlyphPositioningTable.createSubtable(type, lid, sequence, flags, format, coverage, entries);
+               }
+               return st;
+       }
+
+       private void initATState() {
+               seScripts = new java.util.LinkedHashMap();
+               seLanguages = new java.util.LinkedHashMap();
+               seFeatures = new java.util.LinkedHashMap();
+               seSubtables = new java.util.ArrayList();
+               resetATSubState();
+       }
+
+       private void resetATState() {
+               seScripts = null;
+               seLanguages = null;
+               seFeatures = null;
+               seSubtables = null;
+               resetATSubState();
+       }
+
+       private void initATSubState() {
+               seMapping = null;
+               seEntries = new java.util.ArrayList();
+       }
+
+       private void extractSESubState(int tableType, int lookupType, int lookupFlags, int lookupSequence,
+                                                                  int subtableSequence, int subtableFormat) {
+               if (seEntries != null) {
+                       if ((tableType == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION) || (seEntries.size() > 0)) {
+                               if (seSubtables != null) {
+                                       Integer tt = Integer.valueOf(tableType);
+                                       Integer lt = Integer.valueOf(lookupType);
+                                       Integer ln = Integer.valueOf(lookupSequence);
+                                       Integer lf = Integer.valueOf(lookupFlags);
+                                       Integer sn = Integer.valueOf(subtableSequence);
+                                       Integer sf = Integer.valueOf(subtableFormat);
+                                       seSubtables.add(new Object[]{tt, lt, ln, lf, sn, sf, seMapping, seEntries});
+                               }
+                       }
+               }
+       }
+
+       private void resetATSubState() {
+               seMapping = null;
+               seEntries = null;
+       }
+
+       private void resetATStateAll() {
+               resetATState();
+               gdef = null;
+               gsub = null;
+               gpos = null;
+       }
+
+       /**
+        * helper method for formatting an integer array for output
+        */
+       private String toString(int[] ia) {
+               StringBuffer sb = new StringBuffer();
+               if ((ia == null) || (ia.length == 0)) {
+                       sb.append('-');
+               } else {
+                       boolean first = true;
+                       for (int i = 0; i < ia.length; i++) {
+                               if (!first) {
+                                       sb.append(' ');
+                               } else {
+                                       first = false;
+                               }
+                               sb.append(ia[i]);
+                       }
+               }
+               return sb.toString();
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFLanguage.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFLanguage.java
new file mode 100644 (file)
index 0000000..87b148a
--- /dev/null
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+/**
+ * <p>Language system tags defined by OTF specification. Note that this set and their
+ * values do not correspond with ISO639* or any other language registry.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public final class OTFLanguage {
+
+       public static final String ABAZA = "ABA";
+       public static final String ABKHAZIAN = "ABK";
+       public static final String ADYGHE = "ADY";
+       public static final String AFRIKAANS = "AFK";
+       public static final String AFAR = "AFR";
+       public static final String AGAW = "AGW";
+       public static final String ALSATIAN = "ALS";
+       public static final String ALTAI = "ALT";
+       public static final String AMHARIC = "AMH";
+       public static final String PHONETIC_AMERICANIST = "APPH";
+       public static final String ARABIC = "ARA";
+       public static final String AARI = "ARI";
+       public static final String ARAKANESE = "ARK";
+       public static final String ASSAMESE = "ASM";
+       public static final String ATHAPASKAN = "ATH";
+       public static final String AVAR = "AVR";
+       public static final String AWADHI = "AWA";
+       public static final String AYMARA = "AYM";
+       public static final String AZERI = "AZE";
+       public static final String BADAGA = "BAD";
+       public static final String BAGHELKHANDI = "BAG";
+       public static final String BALKAR = "BAL";
+       public static final String BAULE = "BAU";
+       public static final String BERBER = "BBR";
+       public static final String BENCH = "BCH";
+       public static final String BIBLE_CREE = "BCR";
+       public static final String BELARUSSIAN = "BEL";
+       public static final String BEMBA = "BEM";
+       public static final String BENGALI = "BEN";
+       public static final String BULGARIAN = "BGR";
+       public static final String BHILI = "BHI";
+       public static final String BHOJPURI = "BHO";
+       public static final String BIKOL = "BIK";
+       public static final String BILEN = "BIL";
+       public static final String BLACKFOOT = "BKF";
+       public static final String BALOCHI = "BLI";
+       public static final String BALANTE = "BLN";
+       public static final String BALTI = "BLT";
+       public static final String BAMBARA = "BMB";
+       public static final String BAMILEKE = "BML";
+       public static final String BOSNIAN = "BOS";
+       public static final String BRETON = "BRE";
+       public static final String BRAHUI = "BRH";
+       public static final String BRAJ_BHASHA = "BRI";
+       public static final String BURMESE = "BRM";
+       public static final String BASHKIR = "BSH";
+       public static final String BETI = "BTI";
+       public static final String CATALAN = "CAT";
+       public static final String CEBUANO = "CEB";
+       public static final String CHECHEN = "CHE";
+       public static final String CHAHA_GURAGE = "CHG";
+       public static final String CHATTISGARHI = "CHH";
+       public static final String CHICHEWA = "CHI";
+       public static final String CHUKCHI = "CHK";
+       public static final String CHIPEWYAN = "CHP";
+       public static final String CHEROKEE = "CHR";
+       public static final String CHUVASH = "CHU";
+       public static final String COMORIAN = "CMR";
+       public static final String COPTIC = "COP";
+       public static final String CORSICAN = "COS";
+       public static final String CREE = "CRE";
+       public static final String CARRIER = "CRR";
+       public static final String CRIMEAN_TATAR = "CRT";
+       public static final String CHURCH_SLAVONIC = "CSL";
+       public static final String CZECH = "CSY";
+       public static final String DANISH = "DAN";
+       public static final String DARGWA = "DAR";
+       public static final String WOODS_CREE = "DCR";
+       public static final String GERMAN = "DEU";
+       public static final String DEFAULT = "dflt";
+       public static final String DOGRI = "DGR";
+       public static final String DHIVEHI_DEPRECATED = "DHV";
+       public static final String DHIVEHI = "DIV";
+       public static final String DJERMA = "DJR";
+       public static final String DANGME = "DNG";
+       public static final String DINKA = "DNK";
+       public static final String DARI = "DRI";
+       public static final String DUNGAN = "DUN";
+       public static final String DZONGKHA = "DZN";
+       public static final String EBIRA = "EBI";
+       public static final String EASTERN_CREE = "ECR";
+       public static final String EDO = "EDO";
+       public static final String EFIK = "EFI";
+       public static final String GREEK = "ELL";
+       public static final String ENGLISH = "ENG";
+       public static final String ERZYA = "ERZ";
+       public static final String SPANISH = "ESP";
+       public static final String ESTONIAN = "ETI";
+       public static final String BASQUE = "EUQ";
+       public static final String EVENKI = "EVK";
+       public static final String EVEN = "EVN";
+       public static final String EWE = "EWE";
+       public static final String FRENCH_ANTILLEAN = "FAN";
+       public static final String FARSI = "FAR";
+       public static final String FINNISH = "FIN";
+       public static final String FIJIAN = "FJI";
+       public static final String FLEMISH = "FLE";
+       public static final String FOREST_NENETS = "FNE";
+       public static final String FON = "FON";
+       public static final String FAROESE = "FOS";
+       public static final String FRENCH = "FRA";
+       public static final String FRISIAN = "FRI";
+       public static final String FRIULIAN = "FRL";
+       public static final String FUTA = "FTA";
+       public static final String FULANI = "FUL";
+       public static final String GA = "GAD";
+       public static final String GAELIC = "GAE";
+       public static final String GAGAUZ = "GAG";
+       public static final String GALICIAN = "GAL";
+       public static final String GARSHUNI = "GAR";
+       public static final String GARHWALI = "GAW";
+       public static final String GEEZ = "GEZ";
+       public static final String GILYAK = "GIL";
+       public static final String GUMUZ = "GMZ";
+       public static final String GONDI = "GON";
+       public static final String GREENLANDIC = "GRN";
+       public static final String GARO = "GRO";
+       public static final String GUARANI = "GUA";
+       public static final String GUJARATI = "GUJ";
+       public static final String HAITIAN = "HAI";
+       public static final String HALAM = "HAL";
+       public static final String HARAUTI = "HAR";
+       public static final String HAUSA = "HAU";
+       public static final String HAWAIIN = "HAW";
+       public static final String HAMMER_BANNA = "HBN";
+       public static final String HILIGAYNON = "HIL";
+       public static final String HINDI = "HIN";
+       public static final String HIGH_MARI = "HMA";
+       public static final String HINDKO = "HND";
+       public static final String HO = "HO";
+       public static final String HARARI = "HRI";
+       public static final String CROATIAN = "HRV";
+       public static final String HUNGARIAN = "HUN";
+       public static final String ARMENIAN = "HYE";
+       public static final String IGBO = "IBO";
+       public static final String IJO = "IJO";
+       public static final String ILOKANO = "ILO";
+       public static final String INDONESIAN = "IND";
+       public static final String INGUSH = "ING";
+       public static final String INUKTITUT = "INU";
+       public static final String PHONETIC_IPA = "IPPH";
+       public static final String IRISH = "IRI";
+       public static final String IRISH_TRADITIONAL = "IRT";
+       public static final String ICELANDIC = "ISL";
+       public static final String INARI_SAMI = "ISM";
+       public static final String ITALIAN = "ITA";
+       public static final String HEBREW = "IWR";
+       public static final String JAVANESE = "JAV";
+       public static final String YIDDISH = "JII";
+       public static final String JAPANESE = "JAN";
+       public static final String JUDEZMO = "JUD";
+       public static final String JULA = "JUL";
+       public static final String KABARDIAN = "KAB";
+       public static final String KACHCHI = "KAC";
+       public static final String KALENJIN = "KAL";
+       public static final String KANNADA = "KAN";
+       public static final String KARACHAY = "KAR";
+       public static final String GEORGIAN = "KAT";
+       public static final String KAZAKH = "KAZ";
+       public static final String KEBENA = "KEB";
+       public static final String KHUTSURI_GEORGIAN = "KGE";
+       public static final String KHAKASS = "KHA";
+       public static final String KHANTY_KAZIM = "KHK";
+       public static final String KHMER = "KHM";
+       public static final String KHANTY_SHURISHKAR = "KHS";
+       public static final String KHANTY_VAKHI = "KHV";
+       public static final String KHOWAR = "KHW";
+       public static final String KIKUYU = "KIK";
+       public static final String KIRGHIZ = "KIR";
+       public static final String KISII = "KIS";
+       public static final String KOKNI = "KKN";
+       public static final String KALMYK = "KLM";
+       public static final String KAMBA = "KMB";
+       public static final String KUMAONI = "KMN";
+       public static final String KOMO = "KMO";
+       public static final String KOMSO = "KMS";
+       public static final String KANURI = "KNR";
+       public static final String KODAGU = "KOD";
+       public static final String KOREAN_OLD_HANGUL = "KOH";
+       public static final String KONKANI = "KOK";
+       public static final String KIKONGO = "KON";
+       public static final String KOMI_PERMYAK = "KOP";
+       public static final String KOREAN = "KOR";
+       public static final String KOMI_ZYRIAN = "KOZ";
+       public static final String KPELLE = "KPL";
+       public static final String KRIO = "KRI";
+       public static final String KARAKALPAK = "KRK";
+       public static final String KARELIAN = "KRL";
+       public static final String KARAIM = "KRM";
+       public static final String KAREN = "KRN";
+       public static final String KOORETE = "KRT";
+       public static final String KASHMIRI = "KSH";
+       public static final String KHASI = "KSI";
+       public static final String KILDIN_SAMI = "KSM";
+       public static final String KUI = "KUI";
+       public static final String KULVI = "KUL";
+       public static final String KUMYK = "KUM";
+       public static final String KURDISH = "KUR";
+       public static final String KURUKH = "KUU";
+       public static final String KUY = "KUY";
+       public static final String KORYAK = "KYK";
+       public static final String LADIN = "LAD";
+       public static final String LAHULI = "LAH";
+       public static final String LAK = "LAK";
+       public static final String LAMBANI = "LAM";
+       public static final String LAO = "LAO";
+       public static final String LATIN = "LAT";
+       public static final String LAZ = "LAZ";
+       public static final String L_CREE = "LCR";
+       public static final String LADAKHI = "LDK";
+       public static final String LEZGI = "LEZ";
+       public static final String LINGALA = "LIN";
+       public static final String LOW_MARI = "LMA";
+       public static final String LIMBU = "LMB";
+       public static final String LOMWE = "LMW";
+       public static final String LOWER_SORBIAN = "LSB";
+       public static final String LULE_SAMI = "LSM";
+       public static final String LITHUANIAN = "LTH";
+       public static final String LUXEMBOURGISH = "LTZ";
+       public static final String LUBA = "LUB";
+       public static final String LUGANDA = "LUG";
+       public static final String LUHYA = "LUH";
+       public static final String LUO = "LUO";
+       public static final String LATVIAN = "LVI";
+       public static final String MAJANG = "MAJ";
+       public static final String MAKUA = "MAK";
+       public static final String MALAYALAM_TRADITIONAL = "MAL";
+       public static final String MANSI = "MAN";
+       public static final String MAPUDUNGUN = "MAP";
+       public static final String MARATHI = "MAR";
+       public static final String MARWARI = "MAW";
+       public static final String MBUNDU = "MBN";
+       public static final String MANCHU = "MCH";
+       public static final String MOOSE_CREE = "MCR";
+       public static final String MENDE = "MDE";
+       public static final String MEEN = "MEN";
+       public static final String MIZO = "MIZ";
+       public static final String MACEDONIAN = "MKD";
+       public static final String MALE = "MLE";
+       public static final String MALAGASY = "MLG";
+       public static final String MALINKE = "MLN";
+       public static final String MALAYALAM_REFORMED = "MLR";
+       public static final String MALAY = "MLY";
+       public static final String MANDINKA = "MND";
+       public static final String MONGOLIAN = "MNG";
+       public static final String MANIPURI = "MNI";
+       public static final String MANINKA = "MNK";
+       public static final String MANX_GAELIC = "MNX";
+       public static final String MOHAWK = "MOH";
+       public static final String MOKSHA = "MOK";
+       public static final String MOLDAVIAN = "MOL";
+       public static final String MON = "MON";
+       public static final String MOROCCAN = "MOR";
+       public static final String MAORI = "MRI";
+       public static final String MAITHILI = "MTH";
+       public static final String MALTESE = "MTS";
+       public static final String MUNDARI = "MUN";
+       public static final String NAGA_ASSAMESE = "NAG";
+       public static final String NANAI = "NAN";
+       public static final String NASKAPI = "NAS";
+       public static final String N_CREE = "NCR";
+       public static final String NDEBELE = "NDB";
+       public static final String NDONGA = "NDG";
+       public static final String NEPALI = "NEP";
+       public static final String NEWARI = "NEW";
+       public static final String NAGARI = "NGR";
+       public static final String NORWAY_HOUSE_CREE = "NHC";
+       public static final String NISI = "NIS";
+       public static final String NIUEAN = "NIU";
+       public static final String NKOLE = "NKL";
+       public static final String NKO = "NKO";
+       public static final String DUTCH = "NLD";
+       public static final String NOGAI = "NOG";
+       public static final String NORWEGIAN = "NOR";
+       public static final String NORTHERN_SAMI = "NSM";
+       public static final String NORTHERN_TAI = "NTA";
+       public static final String ESPERANTO = "NTO";
+       public static final String NYNORSK = "NYN";
+       public static final String OCCITAN = "OCI";
+       public static final String OJI_CREE = "OCR";
+       public static final String OJIBWAY = "OJB";
+       public static final String ORIYA = "ORI";
+       public static final String OROMO = "ORO";
+       public static final String OSSETIAN = "OSS";
+       public static final String PALESTINIAN_ARAMAIC = "PAA";
+       public static final String PALI = "PAL";
+       public static final String PUNJABI = "PAN";
+       public static final String PALPA = "PAP";
+       public static final String PASHTO = "PAS";
+       public static final String POLYTONIC_GREEK = "PGR";
+       public static final String FILIPINO = "PIL";
+       public static final String PALAUNG = "PLG";
+       public static final String POLISH = "PLK";
+       public static final String PROVENCAL = "PRO";
+       public static final String PORTUGUESE = "PTG";
+       public static final String CHIN = "QIN";
+       public static final String RAJASTHANI = "RAJ";
+       public static final String R_CREE = "RCR";
+       public static final String RUSSIAN_BURIAT = "RBU";
+       public static final String RIANG = "RIA";
+       public static final String RHAETO_ROMANIC = "RMS";
+       public static final String ROMANIAN = "ROM";
+       public static final String ROMANY = "ROY";
+       public static final String RUSYN = "RSY";
+       public static final String RUANDA = "RUA";
+       public static final String RUSSIAN = "RUS";
+       public static final String SADRI = "SAD";
+       public static final String SANSKRIT = "SAN";
+       public static final String SANTALI = "SAT";
+       public static final String SAYISI = "SAY";
+       public static final String SEKOTA = "SEK";
+       public static final String SELKUP = "SEL";
+       public static final String SANGO = "SGO";
+       public static final String SHAN = "SHN";
+       public static final String SIBE = "SIB";
+       public static final String SIDAMO = "SID";
+       public static final String SILTE_GURAGE = "SIG";
+       public static final String SKOLT_SAMI = "SKS";
+       public static final String SLOVAK = "SKY";
+       public static final String SLAVEY = "SLA";
+       public static final String SLOVENIAN = "SLV";
+       public static final String SOMALI = "SML";
+       public static final String SAMOAN = "SMO";
+       public static final String SENA = "SNA";
+       public static final String SINDHI = "SND";
+       public static final String SINHALESE = "SNH";
+       public static final String SONINKE = "SNK";
+       public static final String SODO_GURAGE = "SOG";
+       public static final String SOTHO = "SOT";
+       public static final String ALBANIAN = "SQI";
+       public static final String SERBIAN = "SRB";
+       public static final String SARAIKI = "SRK";
+       public static final String SERER = "SRR";
+       public static final String SOUTH_SLAVEY = "SSL";
+       public static final String SOUTHERN_SAMI = "SSM";
+       public static final String SURI = "SUR";
+       public static final String SVAN = "SVA";
+       public static final String SWEDISH = "SVE";
+       public static final String SWADAYA_ARAMAIC = "SWA";
+       public static final String SWAHILI = "SWK";
+       public static final String SWAZI = "SWZ";
+       public static final String SUTU = "SXT";
+       public static final String SYRIAC = "SYR";
+       public static final String TABASARAN = "TAB";
+       public static final String TAJIKI = "TAJ";
+       public static final String TAMIL = "TAM";
+       public static final String TATAR = "TAT";
+       public static final String TH_CREE = "TCR";
+       public static final String TELUGU = "TEL";
+       public static final String TONGAN = "TGN";
+       public static final String TIGRE = "TGR";
+       public static final String TIGRINYA = "TGY";
+       public static final String THAI = "THA";
+       public static final String TAHITIAN = "THT";
+       public static final String TIBETAN = "TIB";
+       public static final String TURKMEN = "TKM";
+       public static final String TEMNE = "TMN";
+       public static final String TSWANA = "TNA";
+       public static final String TUNDRA_NENETS = "TNE";
+       public static final String TONGA = "TNG";
+       public static final String TODO = "TOD";
+       public static final String TURKISH = "TRK";
+       public static final String TSONGA = "TSG";
+       public static final String TUROYO_ARAMAIC = "TUA";
+       public static final String TULU = "TUL";
+       public static final String TUVIN = "TUV";
+       public static final String TWI = "TWI";
+       public static final String UDMURT = "UDM";
+       public static final String UKRAINIAN = "UKR";
+       public static final String URDU = "URD";
+       public static final String UPPER_SORBIAN = "USB";
+       public static final String UYGHUR = "UYG";
+       public static final String UZBEK = "UZB";
+       public static final String VENDA = "VEN";
+       public static final String VIETNAMESE = "VIT";
+       public static final String WA = "WA";
+       public static final String WAGDI = "WAG";
+       public static final String WEST_CREE = "WCR";
+       public static final String WELSH = "WEL";
+       public static final String WILDCARD = "*";
+       public static final String WOLOF = "WLF";
+       public static final String TAI_LUE = "XBD";
+       public static final String XHOSA = "XHS";
+       public static final String SAKHA = "YAK";
+       public static final String YORUBA = "YBA";
+       public static final String Y_CREE = "YCR";
+       public static final String YI_CLASSIC = "YIC";
+       public static final String YI_MODERN = "YIM";
+       public static final String CHINESE_HONG_KONG_SAR = "ZHH";
+       public static final String CHINESE_PHONETIC = "ZHP";
+       public static final String CHINESE_SIMPLIFIED = "ZHS";
+       public static final String CHINESE_TRADITIONAL = "ZHT";
+       public static final String ZANDE = "ZND";
+       public static final String ZULU = "ZUL";
+
+       public static boolean isDefault(String language) {
+               return (language != null) && language.equals(DEFAULT);
+       }
+
+       public static boolean isWildCard(String language) {
+               return (language != null) && language.equals(WILDCARD);
+       }
+
+       private OTFLanguage() {
+       }
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFScript.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFScript.java
new file mode 100644 (file)
index 0000000..7b4989a
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+/**
+ * <p>Script tags defined by OTF specification. Note that this set and their
+ * values do not correspond with ISO 15924 or Unicode Script names.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public final class OTFScript {
+
+       public static final String ARABIC = "arab";
+       public static final String ARMENIAN = "armn";
+       public static final String AVESTAN = "avst";
+       public static final String BALINESE = "bali";
+       public static final String BAMUM = "bamu";
+       public static final String BATAK = "batk";
+       public static final String BENGALI = "beng";
+       public static final String BENGALI_V2 = "bng2";
+       public static final String BOPOMOFO = "bopo";
+       public static final String BRAILLE = "brai";
+       public static final String BRAHMI = "brah";
+       public static final String BUGINESE = "bugi";
+       public static final String BUHID = "buhd";
+       public static final String BYZANTINE_MUSIC = "byzm";
+       public static final String CANADIAN_SYLLABICS = "cans";
+       public static final String CARIAN = "cari";
+       public static final String CHAKMA = "cakm";
+       public static final String CHAM = "cham";
+       public static final String CHEROKEE = "cher";
+       public static final String CJK_IDEOGRAPHIC = "hani";
+       public static final String COPTIC = "copt";
+       public static final String CYPRIOT_SYLLABARY = "cprt";
+       public static final String CYRILLIC = "cyrl";
+       public static final String DEFAULT = "DFLT";
+       public static final String DESERET = "dsrt";
+       public static final String DEVANAGARI = "deva";
+       public static final String DEVANAGARI_V2 = "dev2";
+       public static final String EGYPTIAN_HEIROGLYPHS = "egyp";
+       public static final String ETHIOPIC = "ethi";
+       public static final String GEORGIAN = "geor";
+       public static final String GLAGOLITIC = "glag";
+       public static final String GOTHIC = "goth";
+       public static final String GREEK = "grek";
+       public static final String GUJARATI = "gujr";
+       public static final String GUJARATI_V2 = "gjr2";
+       public static final String GURMUKHI = "guru";
+       public static final String GURMUKHI_V2 = "gur2";
+       public static final String HANGUL = "hang";
+       public static final String HANGUL_JAMO = "jamo";
+       public static final String HANUNOO = "hano";
+       public static final String HEBREW = "hebr";
+       public static final String HIRAGANA = "kana";
+       public static final String IMPERIAL_ARAMAIC = "armi";
+       public static final String INSCRIPTIONAL_PAHLAVI = "phli";
+       public static final String INSCRIPTIONAL_PARTHIAN = "prti";
+       public static final String JAVANESE = "java";
+       public static final String KAITHI = "kthi";
+       public static final String KANNADA = "knda";
+       public static final String KANNADA_V2 = "knd2";
+       public static final String KATAKANA = "kana";
+       public static final String KAYAH_LI = "kali";
+       public static final String KHAROSTHI = "khar";
+       public static final String KHMER = "khmr";
+       public static final String LAO = "lao";
+       public static final String LATIN = "latn";
+       public static final String LEPCHA = "lepc";
+       public static final String LIMBU = "limb";
+       public static final String LINEAR_B = "linb";
+       public static final String LISU = "lisu";
+       public static final String LYCIAN = "lyci";
+       public static final String LYDIAN = "lydi";
+       public static final String MALAYALAM = "mlym";
+       public static final String MALAYALAM_V2 = "mlm2";
+       public static final String MANDAIC = "mand";
+       public static final String MATHEMATICAL_ALPHANUMERIC_SYMBOLS = "math";
+       public static final String MEITEI = "mtei";
+       public static final String MEROITIC_CURSIVE = "merc";
+       public static final String MEROITIC_HIEROGLYPHS = "mero";
+       public static final String MONGOLIAN = "mong";
+       public static final String MUSICAL_SYMBOLS = "musc";
+       public static final String MYANMAR = "mymr";
+       public static final String NEW_TAI_LUE = "talu";
+       public static final String NKO = "nko";
+       public static final String OGHAM = "ogam";
+       public static final String OL_CHIKI = "olck";
+       public static final String OLD_ITALIC = "ital";
+       public static final String OLD_PERSIAN_CUNEIFORM = "xpeo";
+       public static final String OLD_SOUTH_ARABIAN = "sarb";
+       public static final String OLD_TURKIC = "orkh";
+       public static final String ORIYA = "orya";
+       public static final String ORIYA_V2 = "ory2";
+       public static final String OSMANYA = "osma";
+       public static final String PHAGS_PA = "phag";
+       public static final String PHOENICIAN = "phnx";
+       public static final String REJANG = "rjng";
+       public static final String RUNIC = "runr";
+       public static final String SAMARITAN = "samr";
+       public static final String SAURASHTRA = "saur";
+       public static final String SHARADA = "shrd";
+       public static final String SHAVIAN = "shaw";
+       public static final String SINHALA = "sinh";
+       public static final String SORA_SOMPENG = "sora";
+       public static final String SUMERO_AKKADIAN_CUNEIFORM = "xsux";
+       public static final String SUNDANESE = "sund";
+       public static final String SYLOTI_NAGRI = "sylo";
+       public static final String SYRIAC = "syrc";
+       public static final String TAGALOG = "tglg";
+       public static final String TAGBANWA = "tagb";
+       public static final String TAI_LE = "tale";
+       public static final String TAI_THAM = "lana";
+       public static final String TAI_VIET = "tavt";
+       public static final String TAKRI = "takr";
+       public static final String TAMIL = "taml";
+       public static final String TAMIL_V2 = "tml2";
+       public static final String TELUGU = "telu";
+       public static final String TELUGU_V2 = "tel2";
+       public static final String THAANA = "thaa";
+       public static final String THAI = "thai";
+       public static final String TIBETAN = "tibt";
+       public static final String TIFINAGH = "tfng";
+       public static final String UGARITIC_CUNEIFORM = "ugar";
+       public static final String VAI = "vai";
+       public static final String WILDCARD = "*";
+       public static final String YI = "yi";
+
+       public static boolean isDefault(String script) {
+               return (script != null) && script.equals(DEFAULT);
+       }
+
+       public static boolean isWildCard(String script) {
+               return (script != null) && script.equals(DEFAULT);
+       }
+
+       private OTFScript() {
+       }
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/SingleByteEncoding.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/SingleByteEncoding.java
new file mode 100644 (file)
index 0000000..7a41fda
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+/**
+ * The interface defines a 1-byte character encoding (with 256 characters).
+ */
+public interface SingleByteEncoding {
+
+       /**
+        * Code point that is used if no code point for a specific character has been found.
+        */
+       char NOT_FOUND_CODE_POINT = '\0';
+
+       /**
+        * Returns the encoding's name.
+        *
+        * @return the name of the encoding
+        */
+       String getName();
+
+       /**
+        * Maps a Unicode character to a code point in the encoding.
+        *
+        * @param c the Unicode character to map
+        * @return the code point in the encoding or 0 (=.notdef) if not found
+        */
+       char mapChar(char c);
+
+       /**
+        * Returns the array of character names for this encoding.
+        *
+        * @return the array of character names
+        * (unmapped code points are represented by a ".notdef" value)
+        */
+       String[] getCharNameMap();
+
+       /**
+        * Returns a character array with Unicode scalar values which can be used to map encoding
+        * code points to Unicode values. Note that this does not return all possible Unicode values
+        * that the encoding maps.
+        *
+        * @return a character array with Unicode scalar values
+        */
+       char[] getUnicodeCharMap();
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Typeface.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Typeface.java
new file mode 100644 (file)
index 0000000..21e6427
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.fonts;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Base class for font classes
+ */
+public abstract class Typeface implements FontMetrics {
+
+       /**
+        * Code point that is used if no code point for a specific character has
+        * been found.
+        */
+       public static final char NOT_FOUND = '#';
+
+       /**
+        * Used to identify whether a font has been used (a character map operation
+        * is used as the trigger). This could just as well be a boolean but is a
+        * long out of statistical interest.
+        */
+       private long charMapOps;
+
+       private Set<Character> warnedChars;
+
+       /**
+        * Get the encoding of the font.
+        *
+        * @return the encoding
+        */
+       public abstract String getEncodingName();
+
+       /**
+        * Map a Unicode character to a code point in the font.
+        *
+        * @param c character to map
+        * @return the mapped character
+        */
+       public abstract char mapChar(char c);
+
+       /**
+        * Used for keeping track of character mapping operations in order to determine if a font
+        * was used at all or not.
+        */
+       protected void notifyMapOperation() {
+               this.charMapOps++;
+       }
+
+       /**
+        * Indicates whether this font had to do any character mapping operations. If that was
+        * not the case, it's an indication that the font has never actually been used.
+        *
+        * @return true if the font had to do any character mapping operations
+        */
+       public boolean hadMappingOperations() {
+               return (this.charMapOps > 0);
+       }
+
+       /**
+        * Determines whether this font contains a particular character/glyph.
+        *
+        * @param c character to check
+        * @return True if the character is supported, Falso otherwise
+        */
+       public abstract boolean hasChar(char c);
+
+       /**
+        * Determines whether the font is a multibyte font.
+        *
+        * @return True if it is multibyte
+        */
+       public boolean isMultiByte() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public int getMaxAscent(int size) {
+               return getAscender(size);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public boolean hasFeature(int tableType, String script, String language, String feature) {
+               return false;
+       }
+
+       /**
+        * Provide proper warning if a glyph is not available.
+        *
+        * @param c the character which is missing.
+        */
+       protected void warnMissingGlyph(char c) {
+               // Give up, character is not available
+               Character ch = Character.valueOf(c);
+               if (warnedChars == null) {
+                       warnedChars = new HashSet<Character>();
+               }
+               if (warnedChars.size() < 8 && !warnedChars.contains(ch)) {
+                       warnedChars.add(ch);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String toString() {
+               StringBuffer sbuf = new StringBuffer(super.toString());
+               sbuf.append('{');
+               sbuf.append(getFullName());
+               sbuf.append('}');
+               return sbuf.toString();
+       }
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/ByteArrayOutputStream.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/ByteArrayOutputStream.java
new file mode 100644 (file)
index 0000000..7c43749
--- /dev/null
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.io;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static com.jaredrummler.fontreader.io.IOUtils.EOF;
+
+/**
+ * This class implements an output stream in which the data is
+ * written into a byte array. The buffer automatically grows as data
+ * is written to it.
+ * <p>
+ * The data can be retrieved using <code>toByteArray()</code> and
+ * <code>toString()</code>.
+ * <p>
+ * Closing a {@code ByteArrayOutputStream} has no effect. The methods in
+ * this class can be called after the stream has been closed without
+ * generating an {@code IOException}.
+ * <p>
+ * This is an alternative implementation of the {@link java.io.ByteArrayOutputStream}
+ * class. The original implementation only allocates 32 bytes at the beginning.
+ * As this class is designed for heavy duty it starts at 1024 bytes. In contrast
+ * to the original it doesn't reallocate the whole memory block but allocates
+ * additional buffers. This way no buffers need to be garbage collected and
+ * the contents don't have to be copied to the new buffer. This class is
+ * designed to behave exactly like the original. The only exception is the
+ * deprecated toString(int) method that has been ignored.
+ */
+public class ByteArrayOutputStream extends OutputStream {
+
+       /**
+        * A singleton empty byte array.
+        */
+       private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+       /**
+        * The list of buffers, which grows and never reduces.
+        */
+       private final List<byte[]> buffers = new ArrayList<>();
+       /**
+        * The index of the current buffer.
+        */
+       private int currentBufferIndex;
+       /**
+        * The total count of bytes in all the filled buffers.
+        */
+       private int filledBufferSum;
+       /**
+        * The current buffer.
+        */
+       private byte[] currentBuffer;
+       /**
+        * The total count of bytes written.
+        */
+       private int count;
+       /**
+        * Flag to indicate if the buffers can be reused after reset
+        */
+       private boolean reuseBuffers = true;
+
+       /**
+        * Creates a new byte array output stream. The buffer capacity is
+        * initially 1024 bytes, though its size increases if necessary.
+        */
+       public ByteArrayOutputStream() {
+               this(1024);
+       }
+
+       /**
+        * Creates a new byte array output stream, with a buffer capacity of
+        * the specified size, in bytes.
+        *
+        * @param size the initial size
+        * @throws IllegalArgumentException if size is negative
+        */
+       public ByteArrayOutputStream(final int size) {
+               if (size < 0) {
+                       throw new IllegalArgumentException(
+                                       "Negative initial size: " + size);
+               }
+               synchronized (this) {
+                       needNewBuffer(size);
+               }
+       }
+
+       /**
+        * Makes a new buffer available either by allocating
+        * a new one or re-cycling an existing one.
+        *
+        * @param newcount the size of the buffer if one is created
+        */
+       private void needNewBuffer(final int newcount) {
+               if (currentBufferIndex < buffers.size() - 1) {
+                       //Recycling old buffer
+                       filledBufferSum += currentBuffer.length;
+
+                       currentBufferIndex++;
+                       currentBuffer = buffers.get(currentBufferIndex);
+               } else {
+                       //Creating new buffer
+                       int newBufferSize;
+                       if (currentBuffer == null) {
+                               newBufferSize = newcount;
+                               filledBufferSum = 0;
+                       } else {
+                               newBufferSize = Math.max(
+                                               currentBuffer.length << 1,
+                                               newcount - filledBufferSum);
+                               filledBufferSum += currentBuffer.length;
+                       }
+
+                       currentBufferIndex++;
+                       currentBuffer = new byte[newBufferSize];
+                       buffers.add(currentBuffer);
+               }
+       }
+
+       /**
+        * Write the bytes to byte array.
+        *
+        * @param b   the bytes to write
+        * @param off The start offset
+        * @param len The number of bytes to write
+        */
+       @Override
+       public void write(final byte[] b, final int off, final int len) {
+               if ((off < 0)
+                               || (off > b.length)
+                               || (len < 0)
+                               || ((off + len) > b.length)
+                               || ((off + len) < 0)) {
+                       throw new IndexOutOfBoundsException();
+               } else if (len == 0) {
+                       return;
+               }
+               synchronized (this) {
+                       final int newcount = count + len;
+                       int remaining = len;
+                       int inBufferPos = count - filledBufferSum;
+                       while (remaining > 0) {
+                               final int part = Math.min(remaining, currentBuffer.length - inBufferPos);
+                               System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part);
+                               remaining -= part;
+                               if (remaining > 0) {
+                                       needNewBuffer(newcount);
+                                       inBufferPos = 0;
+                               }
+                       }
+                       count = newcount;
+               }
+       }
+
+       /**
+        * Write a byte to byte array.
+        *
+        * @param b the byte to write
+        */
+       @Override
+       public synchronized void write(final int b) {
+               int inBufferPos = count - filledBufferSum;
+               if (inBufferPos == currentBuffer.length) {
+                       needNewBuffer(count + 1);
+                       inBufferPos = 0;
+               }
+               currentBuffer[inBufferPos] = (byte) b;
+               count++;
+       }
+
+       /**
+        * Writes the entire contents of the specified input stream to this
+        * byte stream. Bytes from the input stream are read directly into the
+        * internal buffers of this streams.
+        *
+        * @param in the input stream to read from
+        * @return total number of bytes read from the input stream
+        * (and written to this stream)
+        * @throws IOException if an I/O error occurs while reading the input stream
+        * @since 1.4
+        */
+       public synchronized int write(final InputStream in) throws IOException {
+               int readCount = 0;
+               int inBufferPos = count - filledBufferSum;
+               int n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos);
+               while (n != EOF) {
+                       readCount += n;
+                       inBufferPos += n;
+                       count += n;
+                       if (inBufferPos == currentBuffer.length) {
+                               needNewBuffer(currentBuffer.length);
+                               inBufferPos = 0;
+                       }
+                       n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos);
+               }
+               return readCount;
+       }
+
+       /**
+        * Return the current size of the byte array.
+        *
+        * @return the current size of the byte array
+        */
+       public synchronized int size() {
+               return count;
+       }
+
+       /**
+        * Closing a {@code ByteArrayOutputStream} has no effect. The methods in
+        * this class can be called after the stream has been closed without
+        * generating an {@code IOException}.
+        *
+        * @throws IOException never (this method should not declare this exception
+        *                     but it has to now due to backwards compatibility)
+        */
+       @Override
+       public void close() throws IOException {
+               //nop
+       }
+
+       /**
+        * @see java.io.ByteArrayOutputStream#reset()
+        */
+       public synchronized void reset() {
+               count = 0;
+               filledBufferSum = 0;
+               currentBufferIndex = 0;
+               if (reuseBuffers) {
+                       currentBuffer = buffers.get(currentBufferIndex);
+               } else {
+                       //Throw away old buffers
+                       currentBuffer = null;
+                       int size = buffers.get(0).length;
+                       buffers.clear();
+                       needNewBuffer(size);
+                       reuseBuffers = true;
+               }
+       }
+
+       /**
+        * Writes the entire contents of this byte stream to the
+        * specified output stream.
+        *
+        * @param out the output stream to write to
+        * @throws IOException if an I/O error occurs, such as if the stream is closed
+        * @see java.io.ByteArrayOutputStream#writeTo(OutputStream)
+        */
+       public synchronized void writeTo(final OutputStream out) throws IOException {
+               int remaining = count;
+               for (final byte[] buf : buffers) {
+                       final int c = Math.min(buf.length, remaining);
+                       out.write(buf, 0, c);
+                       remaining -= c;
+                       if (remaining == 0) {
+                               break;
+                       }
+               }
+       }
+
+       /**
+        * Fetches entire contents of an <code>InputStream</code> and represent
+        * same data as result InputStream.
+        * <p>
+        * This method is useful where,
+        * <ul>
+        * <li>Source InputStream is slow.</li>
+        * <li>It has network resources associated, so we cannot keep it open for
+        * long time.</li>
+        * <li>It has network timeout associated.</li>
+        * </ul>
+        * It can be used in favor of {@link #toByteArray()}, since it
+        * avoids unnecessary allocation and copy of byte[].<br>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param input Stream to be fully buffered.
+        * @return A fully buffered stream.
+        * @throws IOException if an I/O error occurs
+        * @since 2.0
+        */
+       public static InputStream toBufferedInputStream(final InputStream input)
+                       throws IOException {
+               return toBufferedInputStream(input, 1024);
+       }
+
+       /**
+        * Fetches entire contents of an <code>InputStream</code> and represent
+        * same data as result InputStream.
+        * <p>
+        * This method is useful where,
+        * <ul>
+        * <li>Source InputStream is slow.</li>
+        * <li>It has network resources associated, so we cannot keep it open for
+        * long time.</li>
+        * <li>It has network timeout associated.</li>
+        * </ul>
+        * It can be used in favor of {@link #toByteArray()}, since it
+        * avoids unnecessary allocation and copy of byte[].<br>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param input Stream to be fully buffered.
+        * @param size  the initial buffer size
+        * @return A fully buffered stream.
+        * @throws IOException if an I/O error occurs
+        * @since 2.5
+        */
+       public static InputStream toBufferedInputStream(final InputStream input, int size)
+                       throws IOException {
+               // It does not matter if a ByteArrayOutputStream is not closed as close() is a no-op
+               @SuppressWarnings("resource") final ByteArrayOutputStream output = new ByteArrayOutputStream(size);
+               output.write(input);
+               return output.toInputStream();
+       }
+
+       /**
+        * Gets the current contents of this byte stream as a Input Stream. The
+        * returned stream is backed by buffers of <code>this</code> stream,
+        * avoiding memory allocation and copy, thus saving space and time.<br>
+        *
+        * @return the current contents of this output stream.
+        * @see java.io.ByteArrayOutputStream#toByteArray()
+        * @see #reset()
+        * @since 2.5
+        */
+       public synchronized InputStream toInputStream() {
+               int remaining = count;
+               if (remaining == 0) {
+                       return new ClosedInputStream();
+               }
+               final List<ByteArrayInputStream> list = new ArrayList<ByteArrayInputStream>(buffers.size());
+               for (final byte[] buf : buffers) {
+                       final int c = Math.min(buf.length, remaining);
+                       list.add(new ByteArrayInputStream(buf, 0, c));
+                       remaining -= c;
+                       if (remaining == 0) {
+                               break;
+                       }
+               }
+               reuseBuffers = false;
+               return new SequenceInputStream(Collections.enumeration(list));
+       }
+
+       /**
+        * Gets the curent contents of this byte stream as a byte array.
+        * The result is independent of this stream.
+        *
+        * @return the current contents of this output stream, as a byte array
+        * @see java.io.ByteArrayOutputStream#toByteArray()
+        */
+       public synchronized byte[] toByteArray() {
+               int remaining = count;
+               if (remaining == 0) {
+                       return EMPTY_BYTE_ARRAY;
+               }
+               final byte newbuf[] = new byte[remaining];
+               int pos = 0;
+               for (final byte[] buf : buffers) {
+                       final int c = Math.min(buf.length, remaining);
+                       System.arraycopy(buf, 0, newbuf, pos, c);
+                       pos += c;
+                       remaining -= c;
+                       if (remaining == 0) {
+                               break;
+                       }
+               }
+               return newbuf;
+       }
+
+       /**
+        * Gets the curent contents of this byte stream as a string
+        * using the platform default charset.
+        *
+        * @return the contents of the byte array as a String
+        * @see java.io.ByteArrayOutputStream#toString()
+        * @deprecated 2.5 use {@link #toString(String)} instead
+        */
+       @Override
+       @Deprecated
+       public String toString() {
+               // make explicit the use of the default charset
+               return new String(toByteArray(), Charset.defaultCharset());
+       }
+
+       /**
+        * Gets the curent contents of this byte stream as a string
+        * using the specified encoding.
+        *
+        * @param enc the name of the character encoding
+        * @return the string converted from the byte array
+        * @throws UnsupportedEncodingException if the encoding is not supported
+        * @see java.io.ByteArrayOutputStream#toString(String)
+        */
+       public String toString(final String enc) throws UnsupportedEncodingException {
+               return new String(toByteArray(), enc);
+       }
+
+       /**
+        * Gets the curent contents of this byte stream as a string
+        * using the specified encoding.
+        *
+        * @param charset the character encoding
+        * @return the string converted from the byte array
+        * @see java.io.ByteArrayOutputStream#toString(String)
+        * @since 2.5
+        */
+       public String toString(final Charset charset) {
+               return new String(toByteArray(), charset);
+       }
+
+}
\ No newline at end of file
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/Charsets.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/Charsets.java
new file mode 100644 (file)
index 0000000..3838375
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.io;
+
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Charsets required of every implementation of the Java platform.
+ * <p>
+ * From the Java documentation <a href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">
+ * Standard charsets</a>:
+ * <p>
+ * <cite>Every implementation of the Java platform is required to support the following character encodings. Consult
+ * the release documentation for your implementation to see if any other encodings are supported. Consult the release
+ * documentation for your implementation to see if any other encodings are supported. </cite>
+ * </p>
+ *
+ * <ul>
+ * <li><code>US-ASCII</code><br>
+ * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode character set.</li>
+ * <li><code>ISO-8859-1</code><br>
+ * ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.</li>
+ * <li><code>UTF-8</code><br>
+ * Eight-bit Unicode Transformation Format.</li>
+ * <li><code>UTF-16BE</code><br>
+ * Sixteen-bit Unicode Transformation Format, big-endian byte order.</li>
+ * <li><code>UTF-16LE</code><br>
+ * Sixteen-bit Unicode Transformation Format, little-endian byte order.</li>
+ * <li><code>UTF-16</code><br>
+ * Sixteen-bit Unicode Transformation Format, byte order specified by a mandatory initial byte-order mark (either order
+ * accepted on input, big-endian used on output.)</li>
+ * </ul>
+ *
+ * @see <a href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+ */
+public class Charsets {
+
+       //
+       // This class should only contain Charset instances for required encodings. This guarantees that it will load
+       // correctly and without delay on all Java platforms.
+       //
+
+       /**
+        * Constructs a sorted map from canonical charset names to charset objects required of every implementation of the
+        * Java platform.
+        * <p>
+        * From the Java documentation <a href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">
+        * Standard charsets</a>:
+        * </p>
+        *
+        * @return An immutable, case-insensitive map from canonical charset names to charset objects.
+        * @see Charset#availableCharsets()
+        */
+       public static SortedMap<String, Charset> requiredCharsets() {
+               // maybe cache?
+               // TODO Re-implement on Java 7 to use java.nio.charset.StandardCharsets
+               final TreeMap<String, Charset> m = new TreeMap<String, Charset>(String.CASE_INSENSITIVE_ORDER);
+               m.put(ISO_8859_1.name(), ISO_8859_1);
+               m.put(US_ASCII.name(), US_ASCII);
+               m.put(UTF_16.name(), UTF_16);
+               m.put(UTF_16BE.name(), UTF_16BE);
+               m.put(UTF_16LE.name(), UTF_16LE);
+               m.put(UTF_8.name(), UTF_8);
+               return Collections.unmodifiableSortedMap(m);
+       }
+
+       /**
+        * Returns the given Charset or the default Charset if the given Charset is null.
+        *
+        * @param charset A charset or null.
+        * @return the given Charset or the default Charset if the given Charset is null
+        */
+       public static Charset toCharset(final Charset charset) {
+               return charset == null ? Charset.defaultCharset() : charset;
+       }
+
+       /**
+        * Returns a Charset for the named charset. If the name is null, return the default Charset.
+        *
+        * @param charset The name of the requested charset, may be null.
+        * @return a Charset for the named charset
+        * @throws java.nio.charset.UnsupportedCharsetException If the named charset is unavailable
+        */
+       public static Charset toCharset(final String charset) {
+               return charset == null ? Charset.defaultCharset() : Charset.forName(charset);
+       }
+
+       /**
+        * CharEncodingISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.
+        * <p>
+        * Every implementation of the Java platform is required to support this character encoding.
+        * </p>
+        *
+        * @see <a href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+        */
+       public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+
+       /**
+        * <p>
+        * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set.
+        * </p>
+        * <p>
+        * Every implementation of the Java platform is required to support this character encoding.
+        * </p>
+        *
+        * @see <a href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+        */
+       public static final Charset US_ASCII = Charset.forName("US-ASCII");
+
+       /**
+        * <p>
+        * Sixteen-bit Unicode Transformation Format, The byte order specified by a mandatory initial byte-order mark
+        * (either order accepted on input, big-endian used on output)
+        * </p>
+        * <p>
+        * Every implementation of the Java platform is required to support this character encoding.
+        * </p>
+        *
+        * @see <a href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+        */
+       public static final Charset UTF_16 = Charset.forName("UTF-16");
+
+       /**
+        * <p>
+        * Sixteen-bit Unicode Transformation Format, big-endian byte order.
+        * </p>
+        * <p>
+        * Every implementation of the Java platform is required to support this character encoding.
+        * </p>
+        *
+        * @see <a href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+        */
+       public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
+
+       /**
+        * <p>
+        * Sixteen-bit Unicode Transformation Format, little-endian byte order.
+        * </p>
+        * <p>
+        * Every implementation of the Java platform is required to support this character encoding.
+        * </p>
+        *
+        * @see <a href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+        */
+       public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
+
+       /**
+        * <p>
+        * Eight-bit Unicode Transformation Format.
+        * </p>
+        * <p>
+        * Every implementation of the Java platform is required to support this character encoding.
+        * </p>
+        *
+        * @see <a href="http://docs.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
+        */
+       public static final Charset UTF_8 = Charset.forName("UTF-8");
+
+}
\ No newline at end of file
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/ClosedInputStream.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/ClosedInputStream.java
new file mode 100644 (file)
index 0000000..c268bde
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.io;
+
+import java.io.InputStream;
+
+import static com.jaredrummler.fontreader.io.IOUtils.EOF;
+
+/**
+ * Closed input stream. This stream returns EOF to all attempts to read
+ * something from the stream.
+ * <p>
+ * Typically uses of this class include testing for corner cases in methods
+ * that accept input streams and acting as a sentinel value instead of a
+ * {@code null} input stream.
+ */
+public class ClosedInputStream extends InputStream {
+
+       /**
+        * A singleton.
+        */
+       public static final ClosedInputStream CLOSED_INPUT_STREAM = new ClosedInputStream();
+
+       /**
+        * Returns -1 to indicate that the stream is closed.
+        *
+        * @return always -1
+        */
+       @Override
+       public int read() {
+               return EOF;
+       }
+
+}
\ No newline at end of file
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/IOUtils.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/IOUtils.java
new file mode 100644 (file)
index 0000000..42a51e3
--- /dev/null
@@ -0,0 +1,3051 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.io;
+
+import java.io.*;
+import java.net.*;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.Selector;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * General IO stream manipulation utilities.
+ * <p>
+ * This class provides static utility methods for input/output operations.
+ * <ul>
+ * <li>closeQuietly - these methods close a stream ignoring nulls and exceptions
+ * <li>toXxx/read - these methods read data from a stream
+ * <li>write - these methods write data to a stream
+ * <li>copy - these methods copy all the data from one stream to another
+ * <li>contentEquals - these methods compare the content of two streams
+ * </ul>
+ * <p>
+ * The byte-to-char methods and char-to-byte methods involve a conversion step.
+ * Two methods are provided in each case, one that uses the platform default
+ * encoding and the other which allows you to specify an encoding. You are
+ * encouraged to always specify an encoding because relying on the platform
+ * default can lead to unexpected results, for example when moving from
+ * development to production.
+ * <p>
+ * All the methods in this class that read a stream are buffered internally.
+ * This means that there is no cause to use a <code>BufferedInputStream</code>
+ * or <code>BufferedReader</code>. The default buffer size of 4K has been shown
+ * to be efficient in tests.
+ * <p>
+ * The various copy methods all delegate the actual copying to one of the following methods:
+ * <ul>
+ * <li>{@link #copyLarge(InputStream, OutputStream, byte[])}</li>
+ * <li>{@link #copyLarge(InputStream, OutputStream, long, long, byte[])}</li>
+ * <li>{@link #copyLarge(Reader, Writer, char[])}</li>
+ * <li>{@link #copyLarge(Reader, Writer, long, long, char[])}</li>
+ * </ul>
+ * For example, {@link #copy(InputStream, OutputStream)} calls {@link #copyLarge(InputStream, OutputStream)}
+ * which calls {@link #copy(InputStream, OutputStream, int)} which creates the buffer and calls
+ * {@link #copyLarge(InputStream, OutputStream, byte[])}.
+ * <p>
+ * Applications can re-use buffers by using the underlying methods directly.
+ * This may improve performance for applications that need to do a lot of copying.
+ * <p>
+ * Wherever possible, the methods in this class do <em>not</em> flush or close
+ * the stream. This is to avoid making non-portable assumptions about the
+ * streams' origin and further use. Thus the caller is still responsible for
+ * closing streams after use.
+ * <p>
+ * Origin of code: Excalibur.
+ */
+public class IOUtils {
+       // NOTE: This class is focused on InputStream, OutputStream, Reader and
+       // Writer. Each method should take at least one of these as a parameter,
+       // or return one of them.
+
+       /**
+        * Represents the end-of-file (or stream).
+        */
+       public static final int EOF = -1;
+
+       /**
+        * The Unix directory separator character.
+        */
+       public static final char DIR_SEPARATOR_UNIX = '/';
+       /**
+        * The Windows directory separator character.
+        */
+       public static final char DIR_SEPARATOR_WINDOWS = '\\';
+       /**
+        * The system directory separator character.
+        */
+       public static final char DIR_SEPARATOR = File.separatorChar;
+       /**
+        * The Unix line separator string.
+        */
+       public static final String LINE_SEPARATOR_UNIX = "\n";
+       /**
+        * The Windows line separator string.
+        */
+       public static final String LINE_SEPARATOR_WINDOWS = "\r\n";
+       /**
+        * The system line separator string.
+        */
+       public static final String LINE_SEPARATOR;
+
+       static {
+               // avoid security issues
+               final StringBuilderWriter buf = new StringBuilderWriter(4);
+               final PrintWriter out = new PrintWriter(buf);
+               out.println();
+               LINE_SEPARATOR = buf.toString();
+               out.close();
+       }
+
+       /**
+        * The default buffer size ({@value}) to use for
+        * {@link #copyLarge(InputStream, OutputStream)}
+        * and
+        * {@link #copyLarge(Reader, Writer)}
+        */
+       private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
+
+       /**
+        * The default buffer size to use for the skip() methods.
+        */
+       private static final int SKIP_BUFFER_SIZE = 2048;
+
+       // Allocated in the relevant skip method if necessary.
+       /*
+        * These buffers are static and are shared between threads.
+        * This is possible because the buffers are write-only - the contents are never read.
+        *
+        * N.B. there is no need to synchronize when creating these because:
+        * - we don't care if the buffer is created multiple times (the data is ignored)
+        * - we always use the same size buffer, so if it it is recreated it will still be OK
+        * (if the buffer size were variable, we would need to synch. to ensure some other thread
+        * did not create a smaller one)
+        */
+       private static char[] SKIP_CHAR_BUFFER;
+       private static byte[] SKIP_BYTE_BUFFER;
+
+       /**
+        * Instances should NOT be constructed in standard programming.
+        */
+       public IOUtils() {
+               super();
+       }
+
+       //-----------------------------------------------------------------------
+
+       /**
+        * Closes a URLConnection.
+        *
+        * @param conn the connection to close.
+        * @since 2.4
+        */
+       public static void close(final URLConnection conn) {
+               if (conn instanceof HttpURLConnection) {
+                       ((HttpURLConnection) conn).disconnect();
+               }
+       }
+
+       /**
+        * Closes an <code>Reader</code> unconditionally.
+        * <p>
+        * Equivalent to {@link Reader#close()}, except any exceptions will be ignored.
+        * This is typically used in finally blocks.
+        * <p>
+        * Example code:
+        * <pre>
+        *   char[] data = new char[1024];
+        *   Reader in = null;
+        *   try {
+        *       in = new FileReader("foo.txt");
+        *       in.read(data);
+        *       in.close(); //close errors are handled
+        *   } catch (Exception e) {
+        *       // error handling
+        *   } finally {
+        *       IOUtils.closeQuietly(in);
+        *   }
+        * </pre>
+        *
+        * @param input the Reader to close, may be null or already closed
+        */
+       public static void closeQuietly(final Reader input) {
+               closeQuietly((Closeable) input);
+       }
+
+       /**
+        * Closes an <code>Writer</code> unconditionally.
+        * <p>
+        * Equivalent to {@link Writer#close()}, except any exceptions will be ignored.
+        * This is typically used in finally blocks.
+        * <p>
+        * Example code:
+        * <pre>
+        *   Writer out = null;
+        *   try {
+        *       out = new StringWriter();
+        *       out.write("Hello World");
+        *       out.close(); //close errors are handled
+        *   } catch (Exception e) {
+        *       // error handling
+        *   } finally {
+        *       IOUtils.closeQuietly(out);
+        *   }
+        * </pre>
+        *
+        * @param output the Writer to close, may be null or already closed
+        */
+       public static void closeQuietly(final Writer output) {
+               closeQuietly((Closeable) output);
+       }
+
+       /**
+        * Closes an <code>InputStream</code> unconditionally.
+        * <p>
+        * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored.
+        * This is typically used in finally blocks.
+        * <p>
+        * Example code:
+        * <pre>
+        *   byte[] data = new byte[1024];
+        *   InputStream in = null;
+        *   try {
+        *       in = new FileInputStream("foo.txt");
+        *       in.read(data);
+        *       in.close(); //close errors are handled
+        *   } catch (Exception e) {
+        *       // error handling
+        *   } finally {
+        *       IOUtils.closeQuietly(in);
+        *   }
+        * </pre>
+        *
+        * @param input the InputStream to close, may be null or already closed
+        */
+       public static void closeQuietly(final InputStream input) {
+               closeQuietly((Closeable) input);
+       }
+
+       /**
+        * Closes an <code>OutputStream</code> unconditionally.
+        * <p>
+        * Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored.
+        * This is typically used in finally blocks.
+        * <p>
+        * Example code:
+        * <pre>
+        * byte[] data = "Hello, World".getBytes();
+        *
+        * OutputStream out = null;
+        * try {
+        *     out = new FileOutputStream("foo.txt");
+        *     out.write(data);
+        *     out.close(); //close errors are handled
+        * } catch (IOException e) {
+        *     // error handling
+        * } finally {
+        *     IOUtils.closeQuietly(out);
+        * }
+        * </pre>
+        *
+        * @param output the OutputStream to close, may be null or already closed
+        */
+       public static void closeQuietly(final OutputStream output) {
+               closeQuietly((Closeable) output);
+       }
+
+       /**
+        * Closes a <code>Closeable</code> unconditionally.
+        * <p>
+        * Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. This is typically used in
+        * finally blocks.
+        * <p>
+        * Example code:
+        * </p>
+        * <pre>
+        * Closeable closeable = null;
+        * try {
+        *     closeable = new FileReader(&quot;foo.txt&quot;);
+        *     // process closeable
+        *     closeable.close();
+        * } catch (Exception e) {
+        *     // error handling
+        * } finally {
+        *     IOUtils.closeQuietly(closeable);
+        * }
+        * </pre>
+        * <p>
+        * Closing all streams:
+        * </p>
+        * <pre>
+        * try {
+        *     return IOUtils.copy(inputStream, outputStream);
+        * } finally {
+        *     IOUtils.closeQuietly(inputStream);
+        *     IOUtils.closeQuietly(outputStream);
+        * }
+        * </pre>
+        *
+        * @param closeable the objects to close, may be null or already closed
+        * @since 2.0
+        */
+       public static void closeQuietly(final Closeable closeable) {
+               try {
+                       if (closeable != null) {
+                               closeable.close();
+                       }
+               } catch (final IOException ioe) {
+                       // ignore
+               }
+       }
+
+       /**
+        * Closes a <code>Closeable</code> unconditionally.
+        * <p>
+        * Equivalent to {@link Closeable#close()}, except any exceptions will be ignored.
+        * <p>
+        * This is typically used in finally blocks to ensure that the closeable is closed
+        * even if an Exception was thrown before the normal close statement was reached.
+        * <br>
+        * <b>It should not be used to replace the close statement(s)
+        * which should be present for the non-exceptional case.</b>
+        * <br>
+        * It is only intended to simplify tidying up where normal processing has already failed
+        * and reporting close failure as well is not necessary or useful.
+        * <p>
+        * Example code:
+        * </p>
+        * <pre>
+        * Closeable closeable = null;
+        * try {
+        *     closeable = new FileReader(&quot;foo.txt&quot;);
+        *     // processing using the closeable; may throw an Exception
+        *     closeable.close(); // Normal close - exceptions not ignored
+        * } catch (Exception e) {
+        *     // error handling
+        * } finally {
+        *     <b>IOUtils.closeQuietly(closeable); // In case normal close was skipped due to Exception</b>
+        * }
+        * </pre>
+        * <p>
+        * Closing all streams:
+        * <br>
+        * <pre>
+        * try {
+        *     return IOUtils.copy(inputStream, outputStream);
+        * } finally {
+        *     IOUtils.closeQuietly(inputStream, outputStream);
+        * }
+        * </pre>
+        *
+        * @param closeables the objects to close, may be null or already closed
+        * @see #closeQuietly(Closeable)
+        * @since 2.5
+        */
+       public static void closeQuietly(final Closeable... closeables) {
+               if (closeables == null) {
+                       return;
+               }
+               for (final Closeable closeable : closeables) {
+                       closeQuietly(closeable);
+               }
+       }
+
+       /**
+        * Closes a <code>Socket</code> unconditionally.
+        * <p>
+        * Equivalent to {@link Socket#close()}, except any exceptions will be ignored.
+        * This is typically used in finally blocks.
+        * <p>
+        * Example code:
+        * <pre>
+        *   Socket socket = null;
+        *   try {
+        *       socket = new Socket("http://www.foo.com/", 80);
+        *       // process socket
+        *       socket.close();
+        *   } catch (Exception e) {
+        *       // error handling
+        *   } finally {
+        *       IOUtils.closeQuietly(socket);
+        *   }
+        * </pre>
+        *
+        * @param sock the Socket to close, may be null or already closed
+        * @since 2.0
+        */
+       public static void closeQuietly(final Socket sock) {
+               if (sock != null) {
+                       try {
+                               sock.close();
+                       } catch (final IOException ioe) {
+                               // ignored
+                       }
+               }
+       }
+
+       /**
+        * Closes a <code>Selector</code> unconditionally.
+        * <p>
+        * Equivalent to {@link Selector#close()}, except any exceptions will be ignored.
+        * This is typically used in finally blocks.
+        * <p>
+        * Example code:
+        * <pre>
+        *   Selector selector = null;
+        *   try {
+        *       selector = Selector.open();
+        *       // process socket
+        *
+        *   } catch (Exception e) {
+        *       // error handling
+        *   } finally {
+        *       IOUtils.closeQuietly(selector);
+        *   }
+        * </pre>
+        *
+        * @param selector the Selector to close, may be null or already closed
+        * @since 2.2
+        */
+       public static void closeQuietly(final Selector selector) {
+               if (selector != null) {
+                       try {
+                               selector.close();
+                       } catch (final IOException ioe) {
+                               // ignored
+                       }
+               }
+       }
+
+       /**
+        * Closes a <code>ServerSocket</code> unconditionally.
+        * <p>
+        * Equivalent to {@link ServerSocket#close()}, except any exceptions will be ignored.
+        * This is typically used in finally blocks.
+        * <p>
+        * Example code:
+        * <pre>
+        *   ServerSocket socket = null;
+        *   try {
+        *       socket = new ServerSocket();
+        *       // process socket
+        *       socket.close();
+        *   } catch (Exception e) {
+        *       // error handling
+        *   } finally {
+        *       IOUtils.closeQuietly(socket);
+        *   }
+        * </pre>
+        *
+        * @param sock the ServerSocket to close, may be null or already closed
+        * @since 2.2
+        */
+       public static void closeQuietly(final ServerSocket sock) {
+               if (sock != null) {
+                       try {
+                               sock.close();
+                       } catch (final IOException ioe) {
+                               // ignored
+                       }
+               }
+       }
+
+       /**
+        * Fetches entire contents of an <code>InputStream</code> and represent
+        * same data as result InputStream.
+        * <p>
+        * This method is useful where,
+        * <ul>
+        * <li>Source InputStream is slow.</li>
+        * <li>It has network resources associated, so we cannot keep it open for
+        * long time.</li>
+        * <li>It has network timeout associated.</li>
+        * </ul>
+        * It can be used in favor of {@link #toByteArray(InputStream)}, since it
+        * avoids unnecessary allocation and copy of byte[].<br>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param input Stream to be fully buffered.
+        * @return A fully buffered stream.
+        * @throws IOException if an I/O error occurs
+        * @since 2.0
+        */
+       public static InputStream toBufferedInputStream(final InputStream input) throws IOException {
+               return ByteArrayOutputStream.toBufferedInputStream(input);
+       }
+
+       /**
+        * Fetches entire contents of an <code>InputStream</code> and represent
+        * same data as result InputStream.
+        * <p>
+        * This method is useful where,
+        * <ul>
+        * <li>Source InputStream is slow.</li>
+        * <li>It has network resources associated, so we cannot keep it open for
+        * long time.</li>
+        * <li>It has network timeout associated.</li>
+        * </ul>
+        * It can be used in favor of {@link #toByteArray(InputStream)}, since it
+        * avoids unnecessary allocation and copy of byte[].<br>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param input Stream to be fully buffered.
+        * @param size  the initial buffer size
+        * @return A fully buffered stream.
+        * @throws IOException if an I/O error occurs
+        * @since 2.5
+        */
+       public static InputStream toBufferedInputStream(final InputStream input, int size) throws IOException {
+               return ByteArrayOutputStream.toBufferedInputStream(input, size);
+       }
+
+       /**
+        * Returns the given reader if it is a {@link BufferedReader}, otherwise creates a BufferedReader from the given
+        * reader.
+        *
+        * @param reader the reader to wrap or return (not null)
+        * @return the given reader or a new {@link BufferedReader} for the given reader
+        * @throws NullPointerException if the input parameter is null
+        * @see #buffer(Reader)
+        * @since 2.2
+        */
+       public static BufferedReader toBufferedReader(final Reader reader) {
+               return reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader);
+       }
+
+       /**
+        * Returns the given reader if it is a {@link BufferedReader}, otherwise creates a BufferedReader from the given
+        * reader.
+        *
+        * @param reader the reader to wrap or return (not null)
+        * @param size   the buffer size, if a new BufferedReader is created.
+        * @return the given reader or a new {@link BufferedReader} for the given reader
+        * @throws NullPointerException if the input parameter is null
+        * @see #buffer(Reader)
+        * @since 2.5
+        */
+       public static BufferedReader toBufferedReader(final Reader reader, int size) {
+               return reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader, size);
+       }
+
+       /**
+        * Returns the given reader if it is already a {@link BufferedReader}, otherwise creates a BufferedReader from
+        * the given reader.
+        *
+        * @param reader the reader to wrap or return (not null)
+        * @return the given reader or a new {@link BufferedReader} for the given reader
+        * @throws NullPointerException if the input parameter is null
+        * @since 2.5
+        */
+       public static BufferedReader buffer(final Reader reader) {
+               return reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader);
+       }
+
+       /**
+        * Returns the given reader if it is already a {@link BufferedReader}, otherwise creates a BufferedReader from the
+        * given reader.
+        *
+        * @param reader the reader to wrap or return (not null)
+        * @param size   the buffer size, if a new BufferedReader is created.
+        * @return the given reader or a new {@link BufferedReader} for the given reader
+        * @throws NullPointerException if the input parameter is null
+        * @since 2.5
+        */
+       public static BufferedReader buffer(final Reader reader, int size) {
+               return reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader, size);
+       }
+
+       /**
+        * Returns the given Writer if it is already a {@link BufferedWriter}, otherwise creates a BufferedWriter from the
+        * given Writer.
+        *
+        * @param writer the Writer to wrap or return (not null)
+        * @return the given Writer or a new {@link BufferedWriter} for the given Writer
+        * @throws NullPointerException if the input parameter is null
+        * @since 2.5
+        */
+       public static BufferedWriter buffer(final Writer writer) {
+               return writer instanceof BufferedWriter ? (BufferedWriter) writer : new BufferedWriter(writer);
+       }
+
+       /**
+        * Returns the given Writer if it is already a {@link BufferedWriter}, otherwise creates a BufferedWriter from the
+        * given Writer.
+        *
+        * @param writer the Writer to wrap or return (not null)
+        * @param size   the buffer size, if a new BufferedWriter is created.
+        * @return the given Writer or a new {@link BufferedWriter} for the given Writer
+        * @throws NullPointerException if the input parameter is null
+        * @since 2.5
+        */
+       public static BufferedWriter buffer(final Writer writer, int size) {
+               return writer instanceof BufferedWriter ? (BufferedWriter) writer : new BufferedWriter(writer, size);
+       }
+
+       /**
+        * Returns the given OutputStream if it is already a {@link BufferedOutputStream}, otherwise creates a
+        * BufferedOutputStream from the given OutputStream.
+        *
+        * @param outputStream the OutputStream to wrap or return (not null)
+        * @return the given OutputStream or a new {@link BufferedOutputStream} for the given OutputStream
+        * @throws NullPointerException if the input parameter is null
+        * @since 2.5
+        */
+       public static BufferedOutputStream buffer(final OutputStream outputStream) {
+               // reject null early on rather than waiting for IO operation to fail
+               if (outputStream == null) { // not checked by BufferedOutputStream
+                       throw new NullPointerException();
+               }
+               return outputStream instanceof BufferedOutputStream ?
+                               (BufferedOutputStream) outputStream : new BufferedOutputStream(outputStream);
+       }
+
+       /**
+        * Returns the given OutputStream if it is already a {@link BufferedOutputStream}, otherwise creates a
+        * BufferedOutputStream from the given OutputStream.
+        *
+        * @param outputStream the OutputStream to wrap or return (not null)
+        * @param size         the buffer size, if a new BufferedOutputStream is created.
+        * @return the given OutputStream or a new {@link BufferedOutputStream} for the given OutputStream
+        * @throws NullPointerException if the input parameter is null
+        * @since 2.5
+        */
+       public static BufferedOutputStream buffer(final OutputStream outputStream, int size) {
+               // reject null early on rather than waiting for IO operation to fail
+               if (outputStream == null) { // not checked by BufferedOutputStream
+                       throw new NullPointerException();
+               }
+               return outputStream instanceof BufferedOutputStream ?
+                               (BufferedOutputStream) outputStream : new BufferedOutputStream(outputStream, size);
+       }
+
+       /**
+        * Returns the given InputStream if it is already a {@link BufferedInputStream}, otherwise creates a
+        * BufferedInputStream from the given InputStream.
+        *
+        * @param inputStream the InputStream to wrap or return (not null)
+        * @return the given InputStream or a new {@link BufferedInputStream} for the given InputStream
+        * @throws NullPointerException if the input parameter is null
+        * @since 2.5
+        */
+       public static BufferedInputStream buffer(final InputStream inputStream) {
+               // reject null early on rather than waiting for IO operation to fail
+               if (inputStream == null) { // not checked by BufferedInputStream
+                       throw new NullPointerException();
+               }
+               return inputStream instanceof BufferedInputStream ?
+                               (BufferedInputStream) inputStream : new BufferedInputStream(inputStream);
+       }
+
+       /**
+        * Returns the given InputStream if it is already a {@link BufferedInputStream}, otherwise creates a
+        * BufferedInputStream from the given InputStream.
+        *
+        * @param inputStream the InputStream to wrap or return (not null)
+        * @param size        the buffer size, if a new BufferedInputStream is created.
+        * @return the given InputStream or a new {@link BufferedInputStream} for the given InputStream
+        * @throws NullPointerException if the input parameter is null
+        * @since 2.5
+        */
+       public static BufferedInputStream buffer(final InputStream inputStream, int size) {
+               // reject null early on rather than waiting for IO operation to fail
+               if (inputStream == null) { // not checked by BufferedInputStream
+                       throw new NullPointerException();
+               }
+               return inputStream instanceof BufferedInputStream ?
+                               (BufferedInputStream) inputStream : new BufferedInputStream(inputStream, size);
+       }
+
+       // read toByteArray
+       //-----------------------------------------------------------------------
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a <code>byte[]</code>.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param input the <code>InputStream</code> to read from
+        * @return the requested byte array
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        */
+       public static byte[] toByteArray(final InputStream input) throws IOException {
+               final ByteArrayOutputStream output = new ByteArrayOutputStream();
+               copy(input, output);
+               return output.toByteArray();
+       }
+
+       /**
+        * Gets contents of an <code>InputStream</code> as a <code>byte[]</code>.
+        * Use this method instead of <code>toByteArray(InputStream)</code>
+        * when <code>InputStream</code> size is known.
+        * <b>NOTE:</b> the method checks that the length can safely be cast to an int without truncation
+        * before using {@link IOUtils#toByteArray(java.io.InputStream, int)} to read into the byte array.
+        * (Arrays can have no more than Integer.MAX_VALUE entries anyway)
+        *
+        * @param input the <code>InputStream</code> to read from
+        * @param size  the size of <code>InputStream</code>
+        * @return the requested byte array
+        * @throws IOException              if an I/O error occurs or <code>InputStream</code> size differ from parameter
+        *                                  size
+        * @throws IllegalArgumentException if size is less than zero or size is greater than Integer.MAX_VALUE
+        * @see IOUtils#toByteArray(java.io.InputStream, int)
+        * @since 2.1
+        */
+       public static byte[] toByteArray(final InputStream input, final long size) throws IOException {
+
+               if (size > Integer.MAX_VALUE) {
+                       throw new IllegalArgumentException("Size cannot be greater than Integer max value: " + size);
+               }
+
+               return toByteArray(input, (int) size);
+       }
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a <code>byte[]</code>.
+        * Use this method instead of <code>toByteArray(InputStream)</code>
+        * when <code>InputStream</code> size is known
+        *
+        * @param input the <code>InputStream</code> to read from
+        * @param size  the size of <code>InputStream</code>
+        * @return the requested byte array
+        * @throws IOException              if an I/O error occurs or <code>InputStream</code> size differ from parameter
+        *                                  size
+        * @throws IllegalArgumentException if size is less than zero
+        * @since 2.1
+        */
+       public static byte[] toByteArray(final InputStream input, final int size) throws IOException {
+
+               if (size < 0) {
+                       throw new IllegalArgumentException("Size must be equal or greater than zero: " + size);
+               }
+
+               if (size == 0) {
+                       return new byte[0];
+               }
+
+               final byte[] data = new byte[size];
+               int offset = 0;
+               int readed;
+
+               while (offset < size && (readed = input.read(data, offset, size - offset)) != EOF) {
+                       offset += readed;
+               }
+
+               if (offset != size) {
+                       throw new IOException("Unexpected readed size. current: " + offset + ", excepted: " + size);
+               }
+
+               return data;
+       }
+
+       /**
+        * Gets the contents of a <code>Reader</code> as a <code>byte[]</code>
+        * using the default character encoding of the platform.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        *
+        * @param input the <code>Reader</code> to read from
+        * @return the requested byte array
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @deprecated 2.5 use {@link #toByteArray(Reader, Charset)} instead
+        */
+       @Deprecated
+       public static byte[] toByteArray(final Reader input) throws IOException {
+               return toByteArray(input, Charset.defaultCharset());
+       }
+
+       /**
+        * Gets the contents of a <code>Reader</code> as a <code>byte[]</code>
+        * using the specified character encoding.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        *
+        * @param input    the <code>Reader</code> to read from
+        * @param encoding the encoding to use, null means platform default
+        * @return the requested byte array
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static byte[] toByteArray(final Reader input, final Charset encoding) throws IOException {
+               final ByteArrayOutputStream output = new ByteArrayOutputStream();
+               copy(input, output, encoding);
+               return output.toByteArray();
+       }
+
+       /**
+        * Gets the contents of a <code>Reader</code> as a <code>byte[]</code>
+        * using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        *
+        * @param input    the <code>Reader</code> to read from
+        * @param encoding the encoding to use, null means platform default
+        * @return the requested byte array
+        * @throws NullPointerException                         if the input is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 1.1
+        */
+       public static byte[] toByteArray(final Reader input, final String encoding) throws IOException {
+               return toByteArray(input, Charsets.toCharset(encoding));
+       }
+
+       /**
+        * Gets the contents of a <code>String</code> as a <code>byte[]</code>
+        * using the default character encoding of the platform.
+        * <p>
+        * This is the same as {@link String#getBytes()}.
+        *
+        * @param input the <code>String</code> to convert
+        * @return the requested byte array
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs (never occurs)
+        * @deprecated 2.5 Use {@link String#getBytes()} instead
+        */
+       @Deprecated
+       public static byte[] toByteArray(final String input) throws IOException {
+               // make explicit the use of the default charset
+               return input.getBytes(Charset.defaultCharset());
+       }
+
+       /**
+        * Gets the contents of a <code>URI</code> as a <code>byte[]</code>.
+        *
+        * @param uri the <code>URI</code> to read
+        * @return the requested byte array
+        * @throws NullPointerException if the uri is null
+        * @throws IOException          if an I/O exception occurs
+        * @since 2.4
+        */
+       public static byte[] toByteArray(final URI uri) throws IOException {
+               return IOUtils.toByteArray(uri.toURL());
+       }
+
+       /**
+        * Gets the contents of a <code>URL</code> as a <code>byte[]</code>.
+        *
+        * @param url the <code>URL</code> to read
+        * @return the requested byte array
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O exception occurs
+        * @since 2.4
+        */
+       public static byte[] toByteArray(final URL url) throws IOException {
+               final URLConnection conn = url.openConnection();
+               try {
+                       return IOUtils.toByteArray(conn);
+               } finally {
+                       close(conn);
+               }
+       }
+
+       /**
+        * Gets the contents of a <code>URLConnection</code> as a <code>byte[]</code>.
+        *
+        * @param urlConn the <code>URLConnection</code> to read
+        * @return the requested byte array
+        * @throws NullPointerException if the urlConn is null
+        * @throws IOException          if an I/O exception occurs
+        * @since 2.4
+        */
+       public static byte[] toByteArray(final URLConnection urlConn) throws IOException {
+               final InputStream inputStream = urlConn.getInputStream();
+               try {
+                       return IOUtils.toByteArray(inputStream);
+               } finally {
+                       inputStream.close();
+               }
+       }
+
+       // read char[]
+       //-----------------------------------------------------------------------
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a character array
+        * using the default character encoding of the platform.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param is the <code>InputStream</code> to read from
+        * @return the requested character array
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        * @deprecated 2.5 use {@link #toCharArray(InputStream, Charset)} instead
+        */
+       @Deprecated
+       public static char[] toCharArray(final InputStream is) throws IOException {
+               return toCharArray(is, Charset.defaultCharset());
+       }
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a character array
+        * using the specified character encoding.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param is       the <code>InputStream</code> to read from
+        * @param encoding the encoding to use, null means platform default
+        * @return the requested character array
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static char[] toCharArray(final InputStream is, final Charset encoding)
+                       throws IOException {
+               final CharArrayWriter output = new CharArrayWriter();
+               copy(is, output, encoding);
+               return output.toCharArray();
+       }
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a character array
+        * using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param is       the <code>InputStream</code> to read from
+        * @param encoding the encoding to use, null means platform default
+        * @return the requested character array
+        * @throws NullPointerException                         if the input is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 1.1
+        */
+       public static char[] toCharArray(final InputStream is, final String encoding) throws IOException {
+               return toCharArray(is, Charsets.toCharset(encoding));
+       }
+
+       /**
+        * Gets the contents of a <code>Reader</code> as a character array.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        *
+        * @param input the <code>Reader</code> to read from
+        * @return the requested character array
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        */
+       public static char[] toCharArray(final Reader input) throws IOException {
+               final CharArrayWriter sw = new CharArrayWriter();
+               copy(input, sw);
+               return sw.toCharArray();
+       }
+
+       // read toString
+       //-----------------------------------------------------------------------
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a String
+        * using the default character encoding of the platform.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param input the <code>InputStream</code> to read from
+        * @return the requested String
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @deprecated 2.5 use {@link #toString(InputStream, Charset)} instead
+        */
+       @Deprecated
+       public static String toString(final InputStream input) throws IOException {
+               return toString(input, Charset.defaultCharset());
+       }
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a String
+        * using the specified character encoding.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        * </p>
+        *
+        * @param input    the <code>InputStream</code> to read from
+        * @param encoding the encoding to use, null means platform default
+        * @return the requested String
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static String toString(final InputStream input, final Charset encoding) throws IOException {
+               final StringBuilderWriter sw = new StringBuilderWriter();
+               copy(input, sw, encoding);
+               return sw.toString();
+       }
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a String
+        * using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param input    the <code>InputStream</code> to read from
+        * @param encoding the encoding to use, null means platform default
+        * @return the requested String
+        * @throws NullPointerException                         if the input is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        */
+       public static String toString(final InputStream input, final String encoding)
+                       throws IOException {
+               return toString(input, Charsets.toCharset(encoding));
+       }
+
+       /**
+        * Gets the contents of a <code>Reader</code> as a String.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        *
+        * @param input the <code>Reader</code> to read from
+        * @return the requested String
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        */
+       public static String toString(final Reader input) throws IOException {
+               final StringBuilderWriter sw = new StringBuilderWriter();
+               copy(input, sw);
+               return sw.toString();
+       }
+
+       /**
+        * Gets the contents at the given URI.
+        *
+        * @param uri The URI source.
+        * @return The contents of the URL as a String.
+        * @throws IOException if an I/O exception occurs.
+        * @since 2.1
+        * @deprecated 2.5 use {@link #toString(URI, Charset)} instead
+        */
+       @Deprecated
+       public static String toString(final URI uri) throws IOException {
+               return toString(uri, Charset.defaultCharset());
+       }
+
+       /**
+        * Gets the contents at the given URI.
+        *
+        * @param uri      The URI source.
+        * @param encoding The encoding name for the URL contents.
+        * @return The contents of the URL as a String.
+        * @throws IOException if an I/O exception occurs.
+        * @since 2.3.
+        */
+       public static String toString(final URI uri, final Charset encoding) throws IOException {
+               return toString(uri.toURL(), Charsets.toCharset(encoding));
+       }
+
+       /**
+        * Gets the contents at the given URI.
+        *
+        * @param uri      The URI source.
+        * @param encoding The encoding name for the URL contents.
+        * @return The contents of the URL as a String.
+        * @throws IOException                                  if an I/O exception occurs.
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 2.1
+        */
+       public static String toString(final URI uri, final String encoding) throws IOException {
+               return toString(uri, Charsets.toCharset(encoding));
+       }
+
+       /**
+        * Gets the contents at the given URL.
+        *
+        * @param url The URL source.
+        * @return The contents of the URL as a String.
+        * @throws IOException if an I/O exception occurs.
+        * @since 2.1
+        * @deprecated 2.5 use {@link #toString(URL, Charset)} instead
+        */
+       @Deprecated
+       public static String toString(final URL url) throws IOException {
+               return toString(url, Charset.defaultCharset());
+       }
+
+       /**
+        * Gets the contents at the given URL.
+        *
+        * @param url      The URL source.
+        * @param encoding The encoding name for the URL contents.
+        * @return The contents of the URL as a String.
+        * @throws IOException if an I/O exception occurs.
+        * @since 2.3
+        */
+       public static String toString(final URL url, final Charset encoding) throws IOException {
+               final InputStream inputStream = url.openStream();
+               try {
+                       return toString(inputStream, encoding);
+               } finally {
+                       inputStream.close();
+               }
+       }
+
+       /**
+        * Gets the contents at the given URL.
+        *
+        * @param url      The URL source.
+        * @param encoding The encoding name for the URL contents.
+        * @return The contents of the URL as a String.
+        * @throws IOException                                  if an I/O exception occurs.
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 2.1
+        */
+       public static String toString(final URL url, final String encoding) throws IOException {
+               return toString(url, Charsets.toCharset(encoding));
+       }
+
+       /**
+        * Gets the contents of a <code>byte[]</code> as a String
+        * using the default character encoding of the platform.
+        *
+        * @param input the byte array to read from
+        * @return the requested String
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs (never occurs)
+        * @deprecated 2.5 Use {@link String#String(byte[])} instead
+        */
+       @Deprecated
+       public static String toString(final byte[] input) throws IOException {
+               // make explicit the use of the default charset
+               return new String(input, Charset.defaultCharset());
+       }
+
+       /**
+        * Gets the contents of a <code>byte[]</code> as a String
+        * using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        *
+        * @param input    the byte array to read from
+        * @param encoding the encoding to use, null means platform default
+        * @return the requested String
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs (never occurs)
+        */
+       public static String toString(final byte[] input, final String encoding) throws IOException {
+               return new String(input, Charsets.toCharset(encoding));
+       }
+
+       // readLines
+       //-----------------------------------------------------------------------
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a list of Strings,
+        * one entry per line, using the default character encoding of the platform.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param input the <code>InputStream</code> to read from, not null
+        * @return the list of Strings, never null
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        * @deprecated 2.5 use {@link #readLines(InputStream, Charset)} instead
+        */
+       @Deprecated
+       public static List<String> readLines(final InputStream input) throws IOException {
+               return readLines(input, Charset.defaultCharset());
+       }
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a list of Strings,
+        * one entry per line, using the specified character encoding.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param input    the <code>InputStream</code> to read from, not null
+        * @param encoding the encoding to use, null means platform default
+        * @return the list of Strings, never null
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static List<String> readLines(final InputStream input, final Charset encoding) throws IOException {
+               final InputStreamReader reader = new InputStreamReader(input, Charsets.toCharset(encoding));
+               return readLines(reader);
+       }
+
+       /**
+        * Gets the contents of an <code>InputStream</code> as a list of Strings,
+        * one entry per line, using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        *
+        * @param input    the <code>InputStream</code> to read from, not null
+        * @param encoding the encoding to use, null means platform default
+        * @return the list of Strings, never null
+        * @throws NullPointerException                         if the input is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 1.1
+        */
+       public static List<String> readLines(final InputStream input, final String encoding) throws IOException {
+               return readLines(input, Charsets.toCharset(encoding));
+       }
+
+       /**
+        * Gets the contents of a <code>Reader</code> as a list of Strings,
+        * one entry per line.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        *
+        * @param input the <code>Reader</code> to read from, not null
+        * @return the list of Strings, never null
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        */
+       public static List<String> readLines(final Reader input) throws IOException {
+               final BufferedReader reader = toBufferedReader(input);
+               final List<String> list = new ArrayList<String>();
+               String line = reader.readLine();
+               while (line != null) {
+                       list.add(line);
+                       line = reader.readLine();
+               }
+               return list;
+       }
+
+       // lineIterator
+       //-----------------------------------------------------------------------
+
+       /**
+        * Returns an Iterator for the lines in a <code>Reader</code>.
+        * <p>
+        * <code>LineIterator</code> holds a reference to the open
+        * <code>Reader</code> specified here. When you have finished with the
+        * iterator you should close the reader to free internal resources.
+        * This can be done by closing the reader directly, or by calling
+        * {@link LineIterator#close()} or {@link LineIterator#closeQuietly(LineIterator)}.
+        * <p>
+        * The recommended usage pattern is:
+        * <pre>
+        * try {
+        *   LineIterator it = IOUtils.lineIterator(reader);
+        *   while (it.hasNext()) {
+        *     String line = it.nextLine();
+        *     /// do something with line
+        *   }
+        * } finally {
+        *   IOUtils.closeQuietly(reader);
+        * }
+        * </pre>
+        *
+        * @param reader the <code>Reader</code> to read from, not null
+        * @return an Iterator of the lines in the reader, never null
+        * @throws IllegalArgumentException if the reader is null
+        * @since 1.2
+        */
+       public static LineIterator lineIterator(final Reader reader) {
+               return new LineIterator(reader);
+       }
+
+       /**
+        * Returns an Iterator for the lines in an <code>InputStream</code>, using
+        * the character encoding specified (or default encoding if null).
+        * <p>
+        * <code>LineIterator</code> holds a reference to the open
+        * <code>InputStream</code> specified here. When you have finished with
+        * the iterator you should close the stream to free internal resources.
+        * This can be done by closing the stream directly, or by calling
+        * {@link LineIterator#close()} or {@link LineIterator#closeQuietly(LineIterator)}.
+        * <p>
+        * The recommended usage pattern is:
+        * <pre>
+        * try {
+        *   LineIterator it = IOUtils.lineIterator(stream, charset);
+        *   while (it.hasNext()) {
+        *     String line = it.nextLine();
+        *     /// do something with line
+        *   }
+        * } finally {
+        *   IOUtils.closeQuietly(stream);
+        * }
+        * </pre>
+        *
+        * @param input    the <code>InputStream</code> to read from, not null
+        * @param encoding the encoding to use, null means platform default
+        * @return an Iterator of the lines in the reader, never null
+        * @throws IllegalArgumentException if the input is null
+        * @throws IOException              if an I/O error occurs, such as if the encoding is invalid
+        * @since 2.3
+        */
+       public static LineIterator lineIterator(final InputStream input, final Charset encoding) throws IOException {
+               return new LineIterator(new InputStreamReader(input, Charsets.toCharset(encoding)));
+       }
+
+       /**
+        * Returns an Iterator for the lines in an <code>InputStream</code>, using
+        * the character encoding specified (or default encoding if null).
+        * <p>
+        * <code>LineIterator</code> holds a reference to the open
+        * <code>InputStream</code> specified here. When you have finished with
+        * the iterator you should close the stream to free internal resources.
+        * This can be done by closing the stream directly, or by calling
+        * {@link LineIterator#close()} or {@link LineIterator#closeQuietly(LineIterator)}.
+        * <p>
+        * The recommended usage pattern is:
+        * <pre>
+        * try {
+        *   LineIterator it = IOUtils.lineIterator(stream, "UTF-8");
+        *   while (it.hasNext()) {
+        *     String line = it.nextLine();
+        *     /// do something with line
+        *   }
+        * } finally {
+        *   IOUtils.closeQuietly(stream);
+        * }
+        * </pre>
+        *
+        * @param input    the <code>InputStream</code> to read from, not null
+        * @param encoding the encoding to use, null means platform default
+        * @return an Iterator of the lines in the reader, never null
+        * @throws IllegalArgumentException                     if the input is null
+        * @throws IOException                                  if an I/O error occurs, such as if the encoding is invalid
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 1.2
+        */
+       public static LineIterator lineIterator(final InputStream input, final String encoding) throws IOException {
+               return lineIterator(input, Charsets.toCharset(encoding));
+       }
+
+       //-----------------------------------------------------------------------
+
+       /**
+        * Converts the specified CharSequence to an input stream, encoded as bytes
+        * using the default character encoding of the platform.
+        *
+        * @param input the CharSequence to convert
+        * @return an input stream
+        * @since 2.0
+        * @deprecated 2.5 use {@link #toInputStream(CharSequence, Charset)} instead
+        */
+       @Deprecated
+       public static InputStream toInputStream(final CharSequence input) {
+               return toInputStream(input, Charset.defaultCharset());
+       }
+
+       /**
+        * Converts the specified CharSequence to an input stream, encoded as bytes
+        * using the specified character encoding.
+        *
+        * @param input    the CharSequence to convert
+        * @param encoding the encoding to use, null means platform default
+        * @return an input stream
+        * @since 2.3
+        */
+       public static InputStream toInputStream(final CharSequence input, final Charset encoding) {
+               return toInputStream(input.toString(), encoding);
+       }
+
+       /**
+        * Converts the specified CharSequence to an input stream, encoded as bytes
+        * using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        *
+        * @param input    the CharSequence to convert
+        * @param encoding the encoding to use, null means platform default
+        * @return an input stream
+        * @throws IOException                                  if the encoding is invalid
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 2.0
+        */
+       public static InputStream toInputStream(final CharSequence input, final String encoding) throws IOException {
+               return toInputStream(input, Charsets.toCharset(encoding));
+       }
+
+       //-----------------------------------------------------------------------
+
+       /**
+        * Converts the specified string to an input stream, encoded as bytes
+        * using the default character encoding of the platform.
+        *
+        * @param input the string to convert
+        * @return an input stream
+        * @since 1.1
+        * @deprecated 2.5 use {@link #toInputStream(String, Charset)} instead
+        */
+       @Deprecated
+       public static InputStream toInputStream(final String input) {
+               return toInputStream(input, Charset.defaultCharset());
+       }
+
+       /**
+        * Converts the specified string to an input stream, encoded as bytes
+        * using the specified character encoding.
+        *
+        * @param input    the string to convert
+        * @param encoding the encoding to use, null means platform default
+        * @return an input stream
+        * @since 2.3
+        */
+       public static InputStream toInputStream(final String input, final Charset encoding) {
+               return new ByteArrayInputStream(input.getBytes(Charsets.toCharset(encoding)));
+       }
+
+       /**
+        * Converts the specified string to an input stream, encoded as bytes
+        * using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        *
+        * @param input    the string to convert
+        * @param encoding the encoding to use, null means platform default
+        * @return an input stream
+        * @throws IOException                                  if the encoding is invalid
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 1.1
+        */
+       public static InputStream toInputStream(final String input, final String encoding) throws IOException {
+               final byte[] bytes = input.getBytes(Charsets.toCharset(encoding));
+               return new ByteArrayInputStream(bytes);
+       }
+
+       // write byte[]
+       //-----------------------------------------------------------------------
+
+       /**
+        * Writes bytes from a <code>byte[]</code> to an <code>OutputStream</code>.
+        *
+        * @param data   the byte array to write, do not modify during output,
+        *               null ignored
+        * @param output the <code>OutputStream</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        */
+       public static void write(final byte[] data, final OutputStream output)
+                       throws IOException {
+               if (data != null) {
+                       output.write(data);
+               }
+       }
+
+       /**
+        * Writes bytes from a <code>byte[]</code> to an <code>OutputStream</code> using chunked writes.
+        * This is intended for writing very large byte arrays which might otherwise cause excessive
+        * memory usage if the native code has to allocate a copy.
+        *
+        * @param data   the byte array to write, do not modify during output,
+        *               null ignored
+        * @param output the <code>OutputStream</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.5
+        */
+       public static void writeChunked(final byte[] data, final OutputStream output)
+                       throws IOException {
+               if (data != null) {
+                       int bytes = data.length;
+                       int offset = 0;
+                       while (bytes > 0) {
+                               int chunk = Math.min(bytes, DEFAULT_BUFFER_SIZE);
+                               output.write(data, offset, chunk);
+                               bytes -= chunk;
+                               offset += chunk;
+                       }
+               }
+       }
+
+       /**
+        * Writes bytes from a <code>byte[]</code> to chars on a <code>Writer</code>
+        * using the default character encoding of the platform.
+        * <p>
+        * This method uses {@link String#String(byte[])}.
+        *
+        * @param data   the byte array to write, do not modify during output,
+        *               null ignored
+        * @param output the <code>Writer</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        * @deprecated 2.5 use {@link #write(byte[], Writer, Charset)} instead
+        */
+       @Deprecated
+       public static void write(final byte[] data, final Writer output) throws IOException {
+               write(data, output, Charset.defaultCharset());
+       }
+
+       /**
+        * Writes bytes from a <code>byte[]</code> to chars on a <code>Writer</code>
+        * using the specified character encoding.
+        * <p>
+        * This method uses {@link String#String(byte[], String)}.
+        *
+        * @param data     the byte array to write, do not modify during output,
+        *                 null ignored
+        * @param output   the <code>Writer</code> to write to
+        * @param encoding the encoding to use, null means platform default
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static void write(final byte[] data, final Writer output, final Charset encoding) throws IOException {
+               if (data != null) {
+                       output.write(new String(data, Charsets.toCharset(encoding)));
+               }
+       }
+
+       /**
+        * Writes bytes from a <code>byte[]</code> to chars on a <code>Writer</code>
+        * using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * This method uses {@link String#String(byte[], String)}.
+        *
+        * @param data     the byte array to write, do not modify during output,
+        *                 null ignored
+        * @param output   the <code>Writer</code> to write to
+        * @param encoding the encoding to use, null means platform default
+        * @throws NullPointerException                         if output is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 1.1
+        */
+       public static void write(final byte[] data, final Writer output, final String encoding) throws IOException {
+               write(data, output, Charsets.toCharset(encoding));
+       }
+
+       // write char[]
+       //-----------------------------------------------------------------------
+
+       /**
+        * Writes chars from a <code>char[]</code> to a <code>Writer</code>
+        *
+        * @param data   the char array to write, do not modify during output,
+        *               null ignored
+        * @param output the <code>Writer</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        */
+       public static void write(final char[] data, final Writer output) throws IOException {
+               if (data != null) {
+                       output.write(data);
+               }
+       }
+
+       /**
+        * Writes chars from a <code>char[]</code> to a <code>Writer</code> using chunked writes.
+        * This is intended for writing very large byte arrays which might otherwise cause excessive
+        * memory usage if the native code has to allocate a copy.
+        *
+        * @param data   the char array to write, do not modify during output,
+        *               null ignored
+        * @param output the <code>Writer</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.5
+        */
+       public static void writeChunked(final char[] data, final Writer output) throws IOException {
+               if (data != null) {
+                       int bytes = data.length;
+                       int offset = 0;
+                       while (bytes > 0) {
+                               int chunk = Math.min(bytes, DEFAULT_BUFFER_SIZE);
+                               output.write(data, offset, chunk);
+                               bytes -= chunk;
+                               offset += chunk;
+                       }
+               }
+       }
+
+       /**
+        * Writes chars from a <code>char[]</code> to bytes on an
+        * <code>OutputStream</code>.
+        * <p>
+        * This method uses {@link String#String(char[])} and
+        * {@link String#getBytes()}.
+        *
+        * @param data   the char array to write, do not modify during output,
+        *               null ignored
+        * @param output the <code>OutputStream</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        * @deprecated 2.5 use {@link #write(char[], OutputStream, Charset)} instead
+        */
+       @Deprecated
+       public static void write(final char[] data, final OutputStream output)
+                       throws IOException {
+               write(data, output, Charset.defaultCharset());
+       }
+
+       /**
+        * Writes chars from a <code>char[]</code> to bytes on an
+        * <code>OutputStream</code> using the specified character encoding.
+        * <p>
+        * This method uses {@link String#String(char[])} and
+        * {@link String#getBytes(String)}.
+        *
+        * @param data     the char array to write, do not modify during output,
+        *                 null ignored
+        * @param output   the <code>OutputStream</code> to write to
+        * @param encoding the encoding to use, null means platform default
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static void write(final char[] data, final OutputStream output, final Charset encoding) throws IOException {
+               if (data != null) {
+                       output.write(new String(data).getBytes(Charsets.toCharset(encoding)));
+               }
+       }
+
+       /**
+        * Writes chars from a <code>char[]</code> to bytes on an
+        * <code>OutputStream</code> using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * This method uses {@link String#String(char[])} and
+        * {@link String#getBytes(String)}.
+        *
+        * @param data     the char array to write, do not modify during output,
+        *                 null ignored
+        * @param output   the <code>OutputStream</code> to write to
+        * @param encoding the encoding to use, null means platform default
+        * @throws NullPointerException                         if output is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the encoding is not supported.
+        * @since 1.1
+        */
+       public static void write(final char[] data, final OutputStream output, final String encoding)
+                       throws IOException {
+               write(data, output, Charsets.toCharset(encoding));
+       }
+
+       // write CharSequence
+       //-----------------------------------------------------------------------
+
+       /**
+        * Writes chars from a <code>CharSequence</code> to a <code>Writer</code>.
+        *
+        * @param data   the <code>CharSequence</code> to write, null ignored
+        * @param output the <code>Writer</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.0
+        */
+       public static void write(final CharSequence data, final Writer output) throws IOException {
+               if (data != null) {
+                       write(data.toString(), output);
+               }
+       }
+
+       /**
+        * Writes chars from a <code>CharSequence</code> to bytes on an
+        * <code>OutputStream</code> using the default character encoding of the
+        * platform.
+        * <p>
+        * This method uses {@link String#getBytes()}.
+        *
+        * @param data   the <code>CharSequence</code> to write, null ignored
+        * @param output the <code>OutputStream</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.0
+        * @deprecated 2.5 use {@link #write(CharSequence, OutputStream, Charset)} instead
+        */
+       @Deprecated
+       public static void write(final CharSequence data, final OutputStream output)
+                       throws IOException {
+               write(data, output, Charset.defaultCharset());
+       }
+
+       /**
+        * Writes chars from a <code>CharSequence</code> to bytes on an
+        * <code>OutputStream</code> using the specified character encoding.
+        * <p>
+        * This method uses {@link String#getBytes(String)}.
+        *
+        * @param data     the <code>CharSequence</code> to write, null ignored
+        * @param output   the <code>OutputStream</code> to write to
+        * @param encoding the encoding to use, null means platform default
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static void write(final CharSequence data, final OutputStream output, final Charset encoding)
+                       throws IOException {
+               if (data != null) {
+                       write(data.toString(), output, encoding);
+               }
+       }
+
+       /**
+        * Writes chars from a <code>CharSequence</code> to bytes on an
+        * <code>OutputStream</code> using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * This method uses {@link String#getBytes(String)}.
+        *
+        * @param data     the <code>CharSequence</code> to write, null ignored
+        * @param output   the <code>OutputStream</code> to write to
+        * @param encoding the encoding to use, null means platform default
+        * @throws NullPointerException                         if output is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the encoding is not supported.
+        * @since 2.0
+        */
+       public static void write(final CharSequence data, final OutputStream output, final String encoding)
+                       throws IOException {
+               write(data, output, Charsets.toCharset(encoding));
+       }
+
+       // write String
+       //-----------------------------------------------------------------------
+
+       /**
+        * Writes chars from a <code>String</code> to a <code>Writer</code>.
+        *
+        * @param data   the <code>String</code> to write, null ignored
+        * @param output the <code>Writer</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        */
+       public static void write(final String data, final Writer output) throws IOException {
+               if (data != null) {
+                       output.write(data);
+               }
+       }
+
+       /**
+        * Writes chars from a <code>String</code> to bytes on an
+        * <code>OutputStream</code> using the default character encoding of the
+        * platform.
+        * <p>
+        * This method uses {@link String#getBytes()}.
+        *
+        * @param data   the <code>String</code> to write, null ignored
+        * @param output the <code>OutputStream</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        * @deprecated 2.5 use {@link #write(String, OutputStream, Charset)} instead
+        */
+       @Deprecated
+       public static void write(final String data, final OutputStream output)
+                       throws IOException {
+               write(data, output, Charset.defaultCharset());
+       }
+
+       /**
+        * Writes chars from a <code>String</code> to bytes on an
+        * <code>OutputStream</code> using the specified character encoding.
+        * <p>
+        * This method uses {@link String#getBytes(String)}.
+        *
+        * @param data     the <code>String</code> to write, null ignored
+        * @param output   the <code>OutputStream</code> to write to
+        * @param encoding the encoding to use, null means platform default
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static void write(final String data, final OutputStream output, final Charset encoding) throws IOException {
+               if (data != null) {
+                       output.write(data.getBytes(Charsets.toCharset(encoding)));
+               }
+       }
+
+       /**
+        * Writes chars from a <code>String</code> to bytes on an
+        * <code>OutputStream</code> using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * This method uses {@link String#getBytes(String)}.
+        *
+        * @param data     the <code>String</code> to write, null ignored
+        * @param output   the <code>OutputStream</code> to write to
+        * @param encoding the encoding to use, null means platform default
+        * @throws NullPointerException                         if output is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the encoding is not supported.
+        * @since 1.1
+        */
+       public static void write(final String data, final OutputStream output, final String encoding)
+                       throws IOException {
+               write(data, output, Charsets.toCharset(encoding));
+       }
+
+       // write StringBuffer
+       //-----------------------------------------------------------------------
+
+       /**
+        * Writes chars from a <code>StringBuffer</code> to a <code>Writer</code>.
+        *
+        * @param data   the <code>StringBuffer</code> to write, null ignored
+        * @param output the <code>Writer</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        * @deprecated replaced by write(CharSequence, Writer)
+        */
+       @Deprecated
+       public static void write(final StringBuffer data, final Writer output)
+                       throws IOException {
+               if (data != null) {
+                       output.write(data.toString());
+               }
+       }
+
+       /**
+        * Writes chars from a <code>StringBuffer</code> to bytes on an
+        * <code>OutputStream</code> using the default character encoding of the
+        * platform.
+        * <p>
+        * This method uses {@link String#getBytes()}.
+        *
+        * @param data   the <code>StringBuffer</code> to write, null ignored
+        * @param output the <code>OutputStream</code> to write to
+        * @throws NullPointerException if output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        * @deprecated replaced by write(CharSequence, OutputStream)
+        */
+       @Deprecated
+       public static void write(final StringBuffer data, final OutputStream output)
+                       throws IOException {
+               write(data, output, (String) null);
+       }
+
+       /**
+        * Writes chars from a <code>StringBuffer</code> to bytes on an
+        * <code>OutputStream</code> using the specified character encoding.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * This method uses {@link String#getBytes(String)}.
+        *
+        * @param data     the <code>StringBuffer</code> to write, null ignored
+        * @param output   the <code>OutputStream</code> to write to
+        * @param encoding the encoding to use, null means platform default
+        * @throws NullPointerException                         if output is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the encoding is not supported.
+        * @since 1.1
+        * @deprecated replaced by write(CharSequence, OutputStream, String)
+        */
+       @Deprecated
+       public static void write(final StringBuffer data, final OutputStream output, final String encoding)
+                       throws IOException {
+               if (data != null) {
+                       output.write(data.toString().getBytes(Charsets.toCharset(encoding)));
+               }
+       }
+
+       // writeLines
+       //-----------------------------------------------------------------------
+
+       /**
+        * Writes the <code>toString()</code> value of each item in a collection to
+        * an <code>OutputStream</code> line by line, using the default character
+        * encoding of the platform and the specified line ending.
+        *
+        * @param lines      the lines to write, null entries produce blank lines
+        * @param lineEnding the line separator to use, null is system default
+        * @param output     the <code>OutputStream</code> to write to, not null, not closed
+        * @throws NullPointerException if the output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        * @deprecated 2.5 use {@link #writeLines(Collection, String, OutputStream, Charset)} instead
+        */
+       @Deprecated
+       public static void writeLines(final Collection<?> lines, final String lineEnding,
+                                                                 final OutputStream output) throws IOException {
+               writeLines(lines, lineEnding, output, Charset.defaultCharset());
+       }
+
+       /**
+        * Writes the <code>toString()</code> value of each item in a collection to
+        * an <code>OutputStream</code> line by line, using the specified character
+        * encoding and the specified line ending.
+        *
+        * @param lines      the lines to write, null entries produce blank lines
+        * @param lineEnding the line separator to use, null is system default
+        * @param output     the <code>OutputStream</code> to write to, not null, not closed
+        * @param encoding   the encoding to use, null means platform default
+        * @throws NullPointerException if the output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static void writeLines(final Collection<?> lines, String lineEnding, final OutputStream output,
+                                                                 final Charset encoding) throws IOException {
+               if (lines == null) {
+                       return;
+               }
+               if (lineEnding == null) {
+                       lineEnding = LINE_SEPARATOR;
+               }
+               final Charset cs = Charsets.toCharset(encoding);
+               for (final Object line : lines) {
+                       if (line != null) {
+                               output.write(line.toString().getBytes(cs));
+                       }
+                       output.write(lineEnding.getBytes(cs));
+               }
+       }
+
+       /**
+        * Writes the <code>toString()</code> value of each item in a collection to
+        * an <code>OutputStream</code> line by line, using the specified character
+        * encoding and the specified line ending.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        *
+        * @param lines      the lines to write, null entries produce blank lines
+        * @param lineEnding the line separator to use, null is system default
+        * @param output     the <code>OutputStream</code> to write to, not null, not closed
+        * @param encoding   the encoding to use, null means platform default
+        * @throws NullPointerException                         if the output is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 1.1
+        */
+       public static void writeLines(final Collection<?> lines, final String lineEnding,
+                                                                 final OutputStream output, final String encoding) throws IOException {
+               writeLines(lines, lineEnding, output, Charsets.toCharset(encoding));
+       }
+
+       /**
+        * Writes the <code>toString()</code> value of each item in a collection to
+        * a <code>Writer</code> line by line, using the specified line ending.
+        *
+        * @param lines      the lines to write, null entries produce blank lines
+        * @param lineEnding the line separator to use, null is system default
+        * @param writer     the <code>Writer</code> to write to, not null, not closed
+        * @throws NullPointerException if the input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        */
+       public static void writeLines(final Collection<?> lines, String lineEnding,
+                                                                 final Writer writer) throws IOException {
+               if (lines == null) {
+                       return;
+               }
+               if (lineEnding == null) {
+                       lineEnding = LINE_SEPARATOR;
+               }
+               for (final Object line : lines) {
+                       if (line != null) {
+                               writer.write(line.toString());
+                       }
+                       writer.write(lineEnding);
+               }
+       }
+
+       // copy from InputStream
+       //-----------------------------------------------------------------------
+
+       /**
+        * Copies bytes from an <code>InputStream</code> to an
+        * <code>OutputStream</code>.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        * <p>
+        * Large streams (over 2GB) will return a bytes copied value of
+        * <code>-1</code> after the copy has completed since the correct
+        * number of bytes cannot be returned as an int. For large streams
+        * use the <code>copyLarge(InputStream, OutputStream)</code> method.
+        *
+        * @param input  the <code>InputStream</code> to read from
+        * @param output the <code>OutputStream</code> to write to
+        * @return the number of bytes copied, or -1 if &gt; Integer.MAX_VALUE
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        */
+       public static int copy(final InputStream input, final OutputStream output) throws IOException {
+               final long count = copyLarge(input, output);
+               if (count > Integer.MAX_VALUE) {
+                       return -1;
+               }
+               return (int) count;
+       }
+
+       /**
+        * Copies bytes from an <code>InputStream</code> to an <code>OutputStream</code> using an internal buffer of the
+        * given size.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a <code>BufferedInputStream</code>.
+        * <p>
+        *
+        * @param input      the <code>InputStream</code> to read from
+        * @param output     the <code>OutputStream</code> to write to
+        * @param bufferSize the bufferSize used to copy from the input to the output
+        * @return the number of bytes copied
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.5
+        */
+       public static long copy(final InputStream input, final OutputStream output, final int bufferSize)
+                       throws IOException {
+               return copyLarge(input, output, new byte[bufferSize]);
+       }
+
+       /**
+        * Copies bytes from a large (over 2GB) <code>InputStream</code> to an
+        * <code>OutputStream</code>.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        * <p>
+        * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
+        *
+        * @param input  the <code>InputStream</code> to read from
+        * @param output the <code>OutputStream</code> to write to
+        * @return the number of bytes copied
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.3
+        */
+       public static long copyLarge(final InputStream input, final OutputStream output)
+                       throws IOException {
+               return copy(input, output, DEFAULT_BUFFER_SIZE);
+       }
+
+       /**
+        * Copies bytes from a large (over 2GB) <code>InputStream</code> to an
+        * <code>OutputStream</code>.
+        * <p>
+        * This method uses the provided buffer, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        * <p>
+        *
+        * @param input  the <code>InputStream</code> to read from
+        * @param output the <code>OutputStream</code> to write to
+        * @param buffer the buffer to use for the copy
+        * @return the number of bytes copied
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.2
+        */
+       public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer)
+                       throws IOException {
+               long count = 0;
+               int n;
+               while (EOF != (n = input.read(buffer))) {
+                       output.write(buffer, 0, n);
+                       count += n;
+               }
+               return count;
+       }
+
+       /**
+        * Copies some or all bytes from a large (over 2GB) <code>InputStream</code> to an
+        * <code>OutputStream</code>, optionally skipping input bytes.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        * </p>
+        * <p>
+        * Note that the implementation uses {@link #skip(InputStream, long)}.
+        * This means that the method may be considerably less efficient than using the actual skip implementation,
+        * this is done to guarantee that the correct number of characters are skipped.
+        * </p>
+        * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
+        *
+        * @param input       the <code>InputStream</code> to read from
+        * @param output      the <code>OutputStream</code> to write to
+        * @param inputOffset : number of bytes to skip from input before copying
+        *                    -ve values are ignored
+        * @param length      : number of bytes to copy. -ve means all
+        * @return the number of bytes copied
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.2
+        */
+       public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset,
+                                                                final long length) throws IOException {
+               return copyLarge(input, output, inputOffset, length, new byte[DEFAULT_BUFFER_SIZE]);
+       }
+
+       /**
+        * Copies some or all bytes from a large (over 2GB) <code>InputStream</code> to an
+        * <code>OutputStream</code>, optionally skipping input bytes.
+        * <p>
+        * This method uses the provided buffer, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        * </p>
+        * <p>
+        * Note that the implementation uses {@link #skip(InputStream, long)}.
+        * This means that the method may be considerably less efficient than using the actual skip implementation,
+        * this is done to guarantee that the correct number of characters are skipped.
+        * </p>
+        *
+        * @param input       the <code>InputStream</code> to read from
+        * @param output      the <code>OutputStream</code> to write to
+        * @param inputOffset : number of bytes to skip from input before copying
+        *                    -ve values are ignored
+        * @param length      : number of bytes to copy. -ve means all
+        * @param buffer      the buffer to use for the copy
+        * @return the number of bytes copied
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.2
+        */
+       public static long copyLarge(final InputStream input, final OutputStream output,
+                                                                final long inputOffset, final long length, final byte[] buffer) throws IOException {
+               if (inputOffset > 0) {
+                       skipFully(input, inputOffset);
+               }
+               if (length == 0) {
+                       return 0;
+               }
+               final int bufferLength = buffer.length;
+               int bytesToRead = bufferLength;
+               if (length > 0 && length < bufferLength) {
+                       bytesToRead = (int) length;
+               }
+               int read;
+               long totalRead = 0;
+               while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) {
+                       output.write(buffer, 0, read);
+                       totalRead += read;
+                       if (length > 0) { // only adjust length if not reading to the end
+                               // Note the cast must work because buffer.length is an integer
+                               bytesToRead = (int) Math.min(length - totalRead, bufferLength);
+                       }
+               }
+               return totalRead;
+       }
+
+       /**
+        * Copies bytes from an <code>InputStream</code> to chars on a
+        * <code>Writer</code> using the default character encoding of the platform.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        * <p>
+        * This method uses {@link InputStreamReader}.
+        *
+        * @param input  the <code>InputStream</code> to read from
+        * @param output the <code>Writer</code> to write to
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        * @deprecated 2.5 use {@link #copy(InputStream, Writer, Charset)} instead
+        */
+       @Deprecated
+       public static void copy(final InputStream input, final Writer output)
+                       throws IOException {
+               copy(input, output, Charset.defaultCharset());
+       }
+
+       /**
+        * Copies bytes from an <code>InputStream</code> to chars on a
+        * <code>Writer</code> using the specified character encoding.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        * <p>
+        * This method uses {@link InputStreamReader}.
+        *
+        * @param input         the <code>InputStream</code> to read from
+        * @param output        the <code>Writer</code> to write to
+        * @param inputEncoding the encoding to use for the input stream, null means platform default
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static void copy(final InputStream input, final Writer output, final Charset inputEncoding)
+                       throws IOException {
+               final InputStreamReader in = new InputStreamReader(input, Charsets.toCharset(inputEncoding));
+               copy(in, output);
+       }
+
+       /**
+        * Copies bytes from an <code>InputStream</code> to chars on a
+        * <code>Writer</code> using the specified character encoding.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedInputStream</code>.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * This method uses {@link InputStreamReader}.
+        *
+        * @param input         the <code>InputStream</code> to read from
+        * @param output        the <code>Writer</code> to write to
+        * @param inputEncoding the encoding to use for the InputStream, null means platform default
+        * @throws NullPointerException                         if the input or output is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 1.1
+        */
+       public static void copy(final InputStream input, final Writer output, final String inputEncoding)
+                       throws IOException {
+               copy(input, output, Charsets.toCharset(inputEncoding));
+       }
+
+       // copy from Reader
+       //-----------------------------------------------------------------------
+
+       /**
+        * Copies chars from a <code>Reader</code> to a <code>Writer</code>.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        * <p>
+        * Large streams (over 2GB) will return a chars copied value of
+        * <code>-1</code> after the copy has completed since the correct
+        * number of chars cannot be returned as an int. For large streams
+        * use the <code>copyLarge(Reader, Writer)</code> method.
+        *
+        * @param input  the <code>Reader</code> to read from
+        * @param output the <code>Writer</code> to write to
+        * @return the number of characters copied, or -1 if &gt; Integer.MAX_VALUE
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        */
+       public static int copy(final Reader input, final Writer output) throws IOException {
+               final long count = copyLarge(input, output);
+               if (count > Integer.MAX_VALUE) {
+                       return -1;
+               }
+               return (int) count;
+       }
+
+       /**
+        * Copies chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        * <p>
+        * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
+        *
+        * @param input  the <code>Reader</code> to read from
+        * @param output the <code>Writer</code> to write to
+        * @return the number of characters copied
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.3
+        */
+       public static long copyLarge(final Reader input, final Writer output) throws IOException {
+               return copyLarge(input, output, new char[DEFAULT_BUFFER_SIZE]);
+       }
+
+       /**
+        * Copies chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>.
+        * <p>
+        * This method uses the provided buffer, so there is no need to use a
+        * <code>BufferedReader</code>.
+        * <p>
+        *
+        * @param input  the <code>Reader</code> to read from
+        * @param output the <code>Writer</code> to write to
+        * @param buffer the buffer to be used for the copy
+        * @return the number of characters copied
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.2
+        */
+       public static long copyLarge(final Reader input, final Writer output, final char[] buffer) throws IOException {
+               long count = 0;
+               int n;
+               while (EOF != (n = input.read(buffer))) {
+                       output.write(buffer, 0, n);
+                       count += n;
+               }
+               return count;
+       }
+
+       /**
+        * Copies some or all chars from a large (over 2GB) <code>InputStream</code> to an
+        * <code>OutputStream</code>, optionally skipping input chars.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        * <p>
+        * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
+        *
+        * @param input       the <code>Reader</code> to read from
+        * @param output      the <code>Writer</code> to write to
+        * @param inputOffset : number of chars to skip from input before copying
+        *                    -ve values are ignored
+        * @param length      : number of chars to copy. -ve means all
+        * @return the number of chars copied
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.2
+        */
+       public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length)
+                       throws IOException {
+               return copyLarge(input, output, inputOffset, length, new char[DEFAULT_BUFFER_SIZE]);
+       }
+
+       /**
+        * Copies some or all chars from a large (over 2GB) <code>InputStream</code> to an
+        * <code>OutputStream</code>, optionally skipping input chars.
+        * <p>
+        * This method uses the provided buffer, so there is no need to use a
+        * <code>BufferedReader</code>.
+        * <p>
+        *
+        * @param input       the <code>Reader</code> to read from
+        * @param output      the <code>Writer</code> to write to
+        * @param inputOffset : number of chars to skip from input before copying
+        *                    -ve values are ignored
+        * @param length      : number of chars to copy. -ve means all
+        * @param buffer      the buffer to be used for the copy
+        * @return the number of chars copied
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.2
+        */
+       public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length,
+                                                                final char[] buffer)
+                       throws IOException {
+               if (inputOffset > 0) {
+                       skipFully(input, inputOffset);
+               }
+               if (length == 0) {
+                       return 0;
+               }
+               int bytesToRead = buffer.length;
+               if (length > 0 && length < buffer.length) {
+                       bytesToRead = (int) length;
+               }
+               int read;
+               long totalRead = 0;
+               while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) {
+                       output.write(buffer, 0, read);
+                       totalRead += read;
+                       if (length > 0) { // only adjust length if not reading to the end
+                               // Note the cast must work because buffer.length is an integer
+                               bytesToRead = (int) Math.min(length - totalRead, buffer.length);
+                       }
+               }
+               return totalRead;
+       }
+
+       /**
+        * Copies chars from a <code>Reader</code> to bytes on an
+        * <code>OutputStream</code> using the default character encoding of the
+        * platform, and calling flush.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        * <p>
+        * Due to the implementation of OutputStreamWriter, this method performs a
+        * flush.
+        * <p>
+        * This method uses {@link OutputStreamWriter}.
+        *
+        * @param input  the <code>Reader</code> to read from
+        * @param output the <code>OutputStream</code> to write to
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        * @deprecated 2.5 use {@link #copy(Reader, OutputStream, Charset)} instead
+        */
+       @Deprecated
+       public static void copy(final Reader input, final OutputStream output)
+                       throws IOException {
+               copy(input, output, Charset.defaultCharset());
+       }
+
+       /**
+        * Copies chars from a <code>Reader</code> to bytes on an
+        * <code>OutputStream</code> using the specified character encoding, and
+        * calling flush.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        * </p>
+        * <p>
+        * Due to the implementation of OutputStreamWriter, this method performs a
+        * flush.
+        * </p>
+        * <p>
+        * This method uses {@link OutputStreamWriter}.
+        * </p>
+        *
+        * @param input          the <code>Reader</code> to read from
+        * @param output         the <code>OutputStream</code> to write to
+        * @param outputEncoding the encoding to use for the OutputStream, null means platform default
+        * @throws NullPointerException if the input or output is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.3
+        */
+       public static void copy(final Reader input, final OutputStream output, final Charset outputEncoding)
+                       throws IOException {
+               final OutputStreamWriter out = new OutputStreamWriter(output, Charsets.toCharset(outputEncoding));
+               copy(input, out);
+               // XXX Unless anyone is planning on rewriting OutputStreamWriter,
+               // we have to flush here.
+               out.flush();
+       }
+
+       /**
+        * Copies chars from a <code>Reader</code> to bytes on an
+        * <code>OutputStream</code> using the specified character encoding, and
+        * calling flush.
+        * <p>
+        * This method buffers the input internally, so there is no need to use a
+        * <code>BufferedReader</code>.
+        * <p>
+        * Character encoding names can be found at
+        * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+        * <p>
+        * Due to the implementation of OutputStreamWriter, this method performs a
+        * flush.
+        * <p>
+        * This method uses {@link OutputStreamWriter}.
+        *
+        * @param input          the <code>Reader</code> to read from
+        * @param output         the <code>OutputStream</code> to write to
+        * @param outputEncoding the encoding to use for the OutputStream, null means platform default
+        * @throws NullPointerException                         if the input or output is null
+        * @throws IOException                                  if an I/O error occurs
+        * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
+        *                                                      .UnsupportedEncodingException} in version 2.2 if the
+        *                                                      encoding is not supported.
+        * @since 1.1
+        */
+       public static void copy(final Reader input, final OutputStream output, final String outputEncoding)
+                       throws IOException {
+               copy(input, output, Charsets.toCharset(outputEncoding));
+       }
+
+       // content equals
+       //-----------------------------------------------------------------------
+
+       /**
+        * Compares the contents of two Streams to determine if they are equal or
+        * not.
+        * <p>
+        * This method buffers the input internally using
+        * <code>BufferedInputStream</code> if they are not already buffered.
+        *
+        * @param input1 the first stream
+        * @param input2 the second stream
+        * @return true if the content of the streams are equal or they both don't
+        * exist, false otherwise
+        * @throws NullPointerException if either input is null
+        * @throws IOException          if an I/O error occurs
+        */
+       public static boolean contentEquals(InputStream input1, InputStream input2)
+                       throws IOException {
+               if (input1 == input2) {
+                       return true;
+               }
+               if (!(input1 instanceof BufferedInputStream)) {
+                       input1 = new BufferedInputStream(input1);
+               }
+               if (!(input2 instanceof BufferedInputStream)) {
+                       input2 = new BufferedInputStream(input2);
+               }
+
+               int ch = input1.read();
+               while (EOF != ch) {
+                       final int ch2 = input2.read();
+                       if (ch != ch2) {
+                               return false;
+                       }
+                       ch = input1.read();
+               }
+
+               final int ch2 = input2.read();
+               return ch2 == EOF;
+       }
+
+       /**
+        * Compares the contents of two Readers to determine if they are equal or
+        * not.
+        * <p>
+        * This method buffers the input internally using
+        * <code>BufferedReader</code> if they are not already buffered.
+        *
+        * @param input1 the first reader
+        * @param input2 the second reader
+        * @return true if the content of the readers are equal or they both don't
+        * exist, false otherwise
+        * @throws NullPointerException if either input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 1.1
+        */
+       public static boolean contentEquals(Reader input1, Reader input2)
+                       throws IOException {
+               if (input1 == input2) {
+                       return true;
+               }
+
+               input1 = toBufferedReader(input1);
+               input2 = toBufferedReader(input2);
+
+               int ch = input1.read();
+               while (EOF != ch) {
+                       final int ch2 = input2.read();
+                       if (ch != ch2) {
+                               return false;
+                       }
+                       ch = input1.read();
+               }
+
+               final int ch2 = input2.read();
+               return ch2 == EOF;
+       }
+
+       /**
+        * Compares the contents of two Readers to determine if they are equal or
+        * not, ignoring EOL characters.
+        * <p>
+        * This method buffers the input internally using
+        * <code>BufferedReader</code> if they are not already buffered.
+        *
+        * @param input1 the first reader
+        * @param input2 the second reader
+        * @return true if the content of the readers are equal (ignoring EOL differences),  false otherwise
+        * @throws NullPointerException if either input is null
+        * @throws IOException          if an I/O error occurs
+        * @since 2.2
+        */
+       public static boolean contentEqualsIgnoreEOL(final Reader input1, final Reader input2)
+                       throws IOException {
+               if (input1 == input2) {
+                       return true;
+               }
+               final BufferedReader br1 = toBufferedReader(input1);
+               final BufferedReader br2 = toBufferedReader(input2);
+
+               String line1 = br1.readLine();
+               String line2 = br2.readLine();
+               while (line1 != null && line2 != null && line1.equals(line2)) {
+                       line1 = br1.readLine();
+                       line2 = br2.readLine();
+               }
+               return line1 == null ? line2 == null ? true : false : line1.equals(line2);
+       }
+
+       /**
+        * Skips bytes from an input byte stream.
+        * This implementation guarantees that it will read as many bytes
+        * as possible before giving up; this may not always be the case for
+        * skip() implementations in subclasses of {@link InputStream}.
+        * <p>
+        * Note that the implementation uses {@link InputStream#read(byte[], int, int)} rather
+        * than delegating to {@link InputStream#skip(long)}.
+        * This means that the method may be considerably less efficient than using the actual skip implementation,
+        * this is done to guarantee that the correct number of bytes are skipped.
+        * </p>
+        *
+        * @param input  byte stream to skip
+        * @param toSkip number of bytes to skip.
+        * @return number of bytes actually skipped.
+        * @throws IOException              if there is a problem reading the file
+        * @throws IllegalArgumentException if toSkip is negative
+        * @see InputStream#skip(long)
+        * @see <a href="https://issues.apache.org/jira/browse/IO-203">IO-203 - Add skipFully() method for InputStreams</a>
+        * @since 2.0
+        */
+       public static long skip(final InputStream input, final long toSkip) throws IOException {
+               if (toSkip < 0) {
+                       throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
+               }
+               /*
+                * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data
+                * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer
+                * size were variable, we would need to synch. to ensure some other thread did not create a smaller one)
+                */
+               if (SKIP_BYTE_BUFFER == null) {
+                       SKIP_BYTE_BUFFER = new byte[SKIP_BUFFER_SIZE];
+               }
+               long remain = toSkip;
+               while (remain > 0) {
+                       // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
+                       final long n = input.read(SKIP_BYTE_BUFFER, 0, (int) Math.min(remain, SKIP_BUFFER_SIZE));
+                       if (n < 0) { // EOF
+                               break;
+                       }
+                       remain -= n;
+               }
+               return toSkip - remain;
+       }
+
+       /**
+        * Skips bytes from a ReadableByteChannel.
+        * This implementation guarantees that it will read as many bytes
+        * as possible before giving up.
+        *
+        * @param input  ReadableByteChannel to skip
+        * @param toSkip number of bytes to skip.
+        * @return number of bytes actually skipped.
+        * @throws IOException              if there is a problem reading the ReadableByteChannel
+        * @throws IllegalArgumentException if toSkip is negative
+        * @since 2.5
+        */
+       public static long skip(final ReadableByteChannel input, final long toSkip) throws IOException {
+               if (toSkip < 0) {
+                       throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
+               }
+               final ByteBuffer skipByteBuffer = ByteBuffer.allocate((int) Math.min(toSkip, SKIP_BUFFER_SIZE));
+               long remain = toSkip;
+               while (remain > 0) {
+                       skipByteBuffer.position(0);
+                       skipByteBuffer.limit((int) Math.min(remain, SKIP_BUFFER_SIZE));
+                       final int n = input.read(skipByteBuffer);
+                       if (n == EOF) {
+                               break;
+                       }
+                       remain -= n;
+               }
+               return toSkip - remain;
+       }
+
+       /**
+        * Skips characters from an input character stream.
+        * This implementation guarantees that it will read as many characters
+        * as possible before giving up; this may not always be the case for
+        * skip() implementations in subclasses of {@link Reader}.
+        * <p>
+        * Note that the implementation uses {@link Reader#read(char[], int, int)} rather
+        * than delegating to {@link Reader#skip(long)}.
+        * This means that the method may be considerably less efficient than using the actual skip implementation,
+        * this is done to guarantee that the correct number of characters are skipped.
+        * </p>
+        *
+        * @param input  character stream to skip
+        * @param toSkip number of characters to skip.
+        * @return number of characters actually skipped.
+        * @throws IOException              if there is a problem reading the file
+        * @throws IllegalArgumentException if toSkip is negative
+        * @see Reader#skip(long)
+        * @see <a href="https://issues.apache.org/jira/browse/IO-203">IO-203 - Add skipFully() method for InputStreams</a>
+        * @since 2.0
+        */
+       public static long skip(final Reader input, final long toSkip) throws IOException {
+               if (toSkip < 0) {
+                       throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
+               }
+               /*
+                * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data
+                * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer
+                * size were variable, we would need to synch. to ensure some other thread did not create a smaller one)
+                */
+               if (SKIP_CHAR_BUFFER == null) {
+                       SKIP_CHAR_BUFFER = new char[SKIP_BUFFER_SIZE];
+               }
+               long remain = toSkip;
+               while (remain > 0) {
+                       // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
+                       final long n = input.read(SKIP_CHAR_BUFFER, 0, (int) Math.min(remain, SKIP_BUFFER_SIZE));
+                       if (n < 0) { // EOF
+                               break;
+                       }
+                       remain -= n;
+               }
+               return toSkip - remain;
+       }
+
+       /**
+        * Skips the requested number of bytes or fail if there are not enough left.
+        * <p>
+        * This allows for the possibility that {@link InputStream#skip(long)} may
+        * not skip as many bytes as requested (most likely because of reaching EOF).
+        * <p>
+        * Note that the implementation uses {@link #skip(InputStream, long)}.
+        * This means that the method may be considerably less efficient than using the actual skip implementation,
+        * this is done to guarantee that the correct number of characters are skipped.
+        * </p>
+        *
+        * @param input  stream to skip
+        * @param toSkip the number of bytes to skip
+        * @throws IOException              if there is a problem reading the file
+        * @throws IllegalArgumentException if toSkip is negative
+        * @throws EOFException             if the number of bytes skipped was incorrect
+        * @see InputStream#skip(long)
+        * @since 2.0
+        */
+       public static void skipFully(final InputStream input, final long toSkip) throws IOException {
+               if (toSkip < 0) {
+                       throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip);
+               }
+               final long skipped = skip(input, toSkip);
+               if (skipped != toSkip) {
+                       throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped);
+               }
+       }
+
+       /**
+        * Skips the requested number of bytes or fail if there are not enough left.
+        *
+        * @param input  ReadableByteChannel to skip
+        * @param toSkip the number of bytes to skip
+        * @throws IOException              if there is a problem reading the ReadableByteChannel
+        * @throws IllegalArgumentException if toSkip is negative
+        * @throws EOFException             if the number of bytes skipped was incorrect
+        * @since 2.5
+        */
+       public static void skipFully(final ReadableByteChannel input, final long toSkip) throws IOException {
+               if (toSkip < 0) {
+                       throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip);
+               }
+               final long skipped = skip(input, toSkip);
+               if (skipped != toSkip) {
+                       throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped);
+               }
+       }
+
+       /**
+        * Skips the requested number of characters or fail if there are not enough left.
+        * <p>
+        * This allows for the possibility that {@link Reader#skip(long)} may
+        * not skip as many characters as requested (most likely because of reaching EOF).
+        * <p>
+        * Note that the implementation uses {@link #skip(Reader, long)}.
+        * This means that the method may be considerably less efficient than using the actual skip implementation,
+        * this is done to guarantee that the correct number of characters are skipped.
+        * </p>
+        *
+        * @param input  stream to skip
+        * @param toSkip the number of characters to skip
+        * @throws IOException              if there is a problem reading the file
+        * @throws IllegalArgumentException if toSkip is negative
+        * @throws EOFException             if the number of characters skipped was incorrect
+        * @see Reader#skip(long)
+        * @since 2.0
+        */
+       public static void skipFully(final Reader input, final long toSkip) throws IOException {
+               final long skipped = skip(input, toSkip);
+               if (skipped != toSkip) {
+                       throw new EOFException("Chars to skip: " + toSkip + " actual: " + skipped);
+               }
+       }
+
+       /**
+        * Reads characters from an input character stream.
+        * This implementation guarantees that it will read as many characters
+        * as possible before giving up; this may not always be the case for
+        * subclasses of {@link Reader}.
+        *
+        * @param input  where to read input from
+        * @param buffer destination
+        * @param offset initial offset into buffer
+        * @param length length to read, must be &gt;= 0
+        * @return actual length read; may be less than requested if EOF was reached
+        * @throws IOException if a read error occurs
+        * @since 2.2
+        */
+       public static int read(final Reader input, final char[] buffer, final int offset, final int length)
+                       throws IOException {
+               if (length < 0) {
+                       throw new IllegalArgumentException("Length must not be negative: " + length);
+               }
+               int remaining = length;
+               while (remaining > 0) {
+                       final int location = length - remaining;
+                       final int count = input.read(buffer, offset + location, remaining);
+                       if (EOF == count) { // EOF
+                               break;
+                       }
+                       remaining -= count;
+               }
+               return length - remaining;
+       }
+
+       /**
+        * Reads characters from an input character stream.
+        * This implementation guarantees that it will read as many characters
+        * as possible before giving up; this may not always be the case for
+        * subclasses of {@link Reader}.
+        *
+        * @param input  where to read input from
+        * @param buffer destination
+        * @return actual length read; may be less than requested if EOF was reached
+        * @throws IOException if a read error occurs
+        * @since 2.2
+        */
+       public static int read(final Reader input, final char[] buffer) throws IOException {
+               return read(input, buffer, 0, buffer.length);
+       }
+
+       /**
+        * Reads bytes from an input stream.
+        * This implementation guarantees that it will read as many bytes
+        * as possible before giving up; this may not always be the case for
+        * subclasses of {@link InputStream}.
+        *
+        * @param input  where to read input from
+        * @param buffer destination
+        * @param offset initial offset into buffer
+        * @param length length to read, must be &gt;= 0
+        * @return actual length read; may be less than requested if EOF was reached
+        * @throws IOException if a read error occurs
+        * @since 2.2
+        */
+       public static int read(final InputStream input, final byte[] buffer, final int offset, final int length)
+                       throws IOException {
+               if (length < 0) {
+                       throw new IllegalArgumentException("Length must not be negative: " + length);
+               }
+               int remaining = length;
+               while (remaining > 0) {
+                       final int location = length - remaining;
+                       final int count = input.read(buffer, offset + location, remaining);
+                       if (EOF == count) { // EOF
+                               break;
+                       }
+                       remaining -= count;
+               }
+               return length - remaining;
+       }
+
+       /**
+        * Reads bytes from an input stream.
+        * This implementation guarantees that it will read as many bytes
+        * as possible before giving up; this may not always be the case for
+        * subclasses of {@link InputStream}.
+        *
+        * @param input  where to read input from
+        * @param buffer destination
+        * @return actual length read; may be less than requested if EOF was reached
+        * @throws IOException if a read error occurs
+        * @since 2.2
+        */
+       public static int read(final InputStream input, final byte[] buffer) throws IOException {
+               return read(input, buffer, 0, buffer.length);
+       }
+
+       /**
+        * Reads bytes from a ReadableByteChannel.
+        * <p>
+        * This implementation guarantees that it will read as many bytes
+        * as possible before giving up; this may not always be the case for
+        * subclasses of {@link ReadableByteChannel}.
+        *
+        * @param input  the byte channel to read
+        * @param buffer byte buffer destination
+        * @return the actual length read; may be less than requested if EOF was reached
+        * @throws IOException if a read error occurs
+        * @since 2.5
+        */
+       public static int read(final ReadableByteChannel input, final ByteBuffer buffer) throws IOException {
+               final int length = buffer.remaining();
+               while (buffer.remaining() > 0) {
+                       final int count = input.read(buffer);
+                       if (EOF == count) { // EOF
+                               break;
+                       }
+               }
+               return length - buffer.remaining();
+       }
+
+       /**
+        * Reads the requested number of characters or fail if there are not enough left.
+        * <p>
+        * This allows for the possibility that {@link Reader#read(char[], int, int)} may
+        * not read as many characters as requested (most likely because of reaching EOF).
+        *
+        * @param input  where to read input from
+        * @param buffer destination
+        * @param offset initial offset into buffer
+        * @param length length to read, must be &gt;= 0
+        * @throws IOException              if there is a problem reading the file
+        * @throws IllegalArgumentException if length is negative
+        * @throws EOFException             if the number of characters read was incorrect
+        * @since 2.2
+        */
+       public static void readFully(final Reader input, final char[] buffer, final int offset, final int length)
+                       throws IOException {
+               final int actual = read(input, buffer, offset, length);
+               if (actual != length) {
+                       throw new EOFException("Length to read: " + length + " actual: " + actual);
+               }
+       }
+
+       /**
+        * Reads the requested number of characters or fail if there are not enough left.
+        * <p>
+        * This allows for the possibility that {@link Reader#read(char[], int, int)} may
+        * not read as many characters as requested (most likely because of reaching EOF).
+        *
+        * @param input  where to read input from
+        * @param buffer destination
+        * @throws IOException              if there is a problem reading the file
+        * @throws IllegalArgumentException if length is negative
+        * @throws EOFException             if the number of characters read was incorrect
+        * @since 2.2
+        */
+       public static void readFully(final Reader input, final char[] buffer) throws IOException {
+               readFully(input, buffer, 0, buffer.length);
+       }
+
+       /**
+        * Reads the requested number of bytes or fail if there are not enough left.
+        * <p>
+        * This allows for the possibility that {@link InputStream#read(byte[], int, int)} may
+        * not read as many bytes as requested (most likely because of reaching EOF).
+        *
+        * @param input  where to read input from
+        * @param buffer destination
+        * @param offset initial offset into buffer
+        * @param length length to read, must be &gt;= 0
+        * @throws IOException              if there is a problem reading the file
+        * @throws IllegalArgumentException if length is negative
+        * @throws EOFException             if the number of bytes read was incorrect
+        * @since 2.2
+        */
+       public static void readFully(final InputStream input, final byte[] buffer, final int offset, final int length)
+                       throws IOException {
+               final int actual = read(input, buffer, offset, length);
+               if (actual != length) {
+                       throw new EOFException("Length to read: " + length + " actual: " + actual);
+               }
+       }
+
+       /**
+        * Reads the requested number of bytes or fail if there are not enough left.
+        * <p>
+        * This allows for the possibility that {@link InputStream#read(byte[], int, int)} may
+        * not read as many bytes as requested (most likely because of reaching EOF).
+        *
+        * @param input  where to read input from
+        * @param buffer destination
+        * @throws IOException              if there is a problem reading the file
+        * @throws IllegalArgumentException if length is negative
+        * @throws EOFException             if the number of bytes read was incorrect
+        * @since 2.2
+        */
+       public static void readFully(final InputStream input, final byte[] buffer) throws IOException {
+               readFully(input, buffer, 0, buffer.length);
+       }
+
+       /**
+        * Reads the requested number of bytes or fail if there are not enough left.
+        * <p>
+        * This allows for the possibility that {@link InputStream#read(byte[], int, int)} may
+        * not read as many bytes as requested (most likely because of reaching EOF).
+        *
+        * @param input  where to read input from
+        * @param length length to read, must be &gt;= 0
+        * @return the bytes read from input
+        * @throws IOException              if there is a problem reading the file
+        * @throws IllegalArgumentException if length is negative
+        * @throws EOFException             if the number of bytes read was incorrect
+        * @since 2.5
+        */
+       public static byte[] readFully(final InputStream input, final int length) throws IOException {
+               final byte[] buffer = new byte[length];
+               readFully(input, buffer, 0, buffer.length);
+               return buffer;
+       }
+
+       /**
+        * Reads the requested number of bytes or fail if there are not enough left.
+        * <p>
+        * This allows for the possibility that {@link ReadableByteChannel#read(ByteBuffer)} may
+        * not read as many bytes as requested (most likely because of reaching EOF).
+        *
+        * @param input  the byte channel to read
+        * @param buffer byte buffer destination
+        * @throws IOException  if there is a problem reading the file
+        * @throws EOFException if the number of bytes read was incorrect
+        * @since 2.5
+        */
+       public static void readFully(final ReadableByteChannel input, final ByteBuffer buffer) throws IOException {
+               final int expected = buffer.remaining();
+               final int actual = read(input, buffer);
+               if (actual != expected) {
+                       throw new EOFException("Length to read: " + expected + " actual: " + actual);
+               }
+       }
+
+}
\ No newline at end of file
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/LineIterator.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/LineIterator.java
new file mode 100644 (file)
index 0000000..e00d048
--- /dev/null
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.io;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * An Iterator over the lines in a <code>Reader</code>.
+ * <p>
+ * <code>LineIterator</code> holds a reference to an open <code>Reader</code>.
+ * When you have finished with the iterator you should close the reader
+ * to free internal resources. This can be done by closing the reader directly,
+ * or by calling the {@link #close()} or {@link #closeQuietly(LineIterator)}
+ * method on the iterator.
+ * <p>
+ * The recommended usage pattern is:
+ * <pre>
+ * LineIterator it = FileUtils.lineIterator(file, "UTF-8");
+ * try {
+ *   while (it.hasNext()) {
+ *     String line = it.nextLine();
+ *     // do something with line
+ *   }
+ * } finally {
+ *   it.close();
+ * }
+ * </pre>
+ */
+public class LineIterator implements Iterator<String> {
+
+       // N.B. This class deliberately does not implement Iterable, see https://issues.apache.org/jira/browse/IO-181
+
+       /**
+        * The reader that is being read.
+        */
+       private final BufferedReader bufferedReader;
+       /**
+        * The current line.
+        */
+       private String cachedLine;
+       /**
+        * A flag indicating if the iterator has been fully read.
+        */
+       private boolean finished = false;
+
+       /**
+        * Constructs an iterator of the lines for a <code>Reader</code>.
+        *
+        * @param reader the <code>Reader</code> to read from, not null
+        * @throws IllegalArgumentException if the reader is null
+        */
+       public LineIterator(final Reader reader) throws IllegalArgumentException {
+               if (reader == null) {
+                       throw new IllegalArgumentException("Reader must not be null");
+               }
+               if (reader instanceof BufferedReader) {
+                       bufferedReader = (BufferedReader) reader;
+               } else {
+                       bufferedReader = new BufferedReader(reader);
+               }
+       }
+
+       //-----------------------------------------------------------------------
+
+       /**
+        * Indicates whether the <code>Reader</code> has more lines.
+        * If there is an <code>IOException</code> then {@link #close()} will
+        * be called on this instance.
+        *
+        * @return {@code true} if the Reader has more lines
+        * @throws IllegalStateException if an IO exception occurs
+        */
+       public boolean hasNext() {
+               if (cachedLine != null) {
+                       return true;
+               } else if (finished) {
+                       return false;
+               } else {
+                       try {
+                               while (true) {
+                                       final String line = bufferedReader.readLine();
+                                       if (line == null) {
+                                               finished = true;
+                                               return false;
+                                       } else if (isValidLine(line)) {
+                                               cachedLine = line;
+                                               return true;
+                                       }
+                               }
+                       } catch (final IOException ioe) {
+                               close();
+                               throw new IllegalStateException(ioe);
+                       }
+               }
+       }
+
+       /**
+        * Overridable method to validate each line that is returned.
+        * This implementation always returns true.
+        *
+        * @param line the line that is to be validated
+        * @return true if valid, false to remove from the iterator
+        */
+       protected boolean isValidLine(final String line) {
+               return true;
+       }
+
+       /**
+        * Returns the next line in the wrapped <code>Reader</code>.
+        *
+        * @return the next line from the input
+        * @throws NoSuchElementException if there is no line to return
+        */
+       public String next() {
+               return nextLine();
+       }
+
+       /**
+        * Returns the next line in the wrapped <code>Reader</code>.
+        *
+        * @return the next line from the input
+        * @throws NoSuchElementException if there is no line to return
+        */
+       public String nextLine() {
+               if (!hasNext()) {
+                       throw new NoSuchElementException("No more lines");
+               }
+               final String currentLine = cachedLine;
+               cachedLine = null;
+               return currentLine;
+       }
+
+       /**
+        * Closes the underlying <code>Reader</code> quietly.
+        * This method is useful if you only want to process the first few
+        * lines of a larger file. If you do not close the iterator
+        * then the <code>Reader</code> remains open.
+        * This method can safely be called multiple times.
+        */
+       public void close() {
+               finished = true;
+               IOUtils.closeQuietly(bufferedReader);
+               cachedLine = null;
+       }
+
+       /**
+        * Unsupported.
+        *
+        * @throws UnsupportedOperationException always
+        */
+       public void remove() {
+               throw new UnsupportedOperationException("Remove unsupported on LineIterator");
+       }
+
+       //-----------------------------------------------------------------------
+
+       /**
+        * Closes the iterator, handling null and ignoring exceptions.
+        *
+        * @param iterator the iterator to close
+        */
+       public static void closeQuietly(final LineIterator iterator) {
+               if (iterator != null) {
+                       iterator.close();
+               }
+       }
+
+}
\ No newline at end of file
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/StringBuilderWriter.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/StringBuilderWriter.java
new file mode 100644 (file)
index 0000000..f19bfe9
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.io;
+
+import java.io.Serializable;
+import java.io.Writer;
+
+/**
+ * {@link Writer} implementation that outputs to a {@link StringBuilder}.
+ * <p>
+ * <strong>NOTE:</strong> This implementation, as an alternative to
+ * <code>java.io.StringWriter</code>, provides an <i>un-synchronized</i>
+ * (i.e. for use in a single thread) implementation for better performance.
+ * For safe usage with multiple {@link Thread}s then
+ * <code>java.io.StringWriter</code> should be used.
+ */
+public class StringBuilderWriter extends Writer implements Serializable {
+
+       private static final long serialVersionUID = -146927496096066153L;
+       private final StringBuilder builder;
+
+       /**
+        * Constructs a new {@link StringBuilder} instance with default capacity.
+        */
+       public StringBuilderWriter() {
+               this.builder = new StringBuilder();
+       }
+
+       /**
+        * Constructs a new {@link StringBuilder} instance with the specified capacity.
+        *
+        * @param capacity The initial capacity of the underlying {@link StringBuilder}
+        */
+       public StringBuilderWriter(final int capacity) {
+               this.builder = new StringBuilder(capacity);
+       }
+
+       /**
+        * Constructs a new instance with the specified {@link StringBuilder}.
+        *
+        * <p>If {@code builder} is null a new instance with default capacity will be created.</p>
+        *
+        * @param builder The String builder. May be null.
+        */
+       public StringBuilderWriter(final StringBuilder builder) {
+               this.builder = builder != null ? builder : new StringBuilder();
+       }
+
+       /**
+        * Appends a single character to this Writer.
+        *
+        * @param value The character to append
+        * @return This writer instance
+        */
+       @Override
+       public Writer append(final char value) {
+               builder.append(value);
+               return this;
+       }
+
+       /**
+        * Appends a character sequence to this Writer.
+        *
+        * @param value The character to append
+        * @return This writer instance
+        */
+       @Override
+       public Writer append(final CharSequence value) {
+               builder.append(value);
+               return this;
+       }
+
+       /**
+        * Appends a portion of a character sequence to the {@link StringBuilder}.
+        *
+        * @param value The character to append
+        * @param start The index of the first character
+        * @param end   The index of the last character + 1
+        * @return This writer instance
+        */
+       @Override
+       public Writer append(final CharSequence value, final int start, final int end) {
+               builder.append(value, start, end);
+               return this;
+       }
+
+       /**
+        * Closing this writer has no effect.
+        */
+       @Override
+       public void close() {
+               // no-op
+       }
+
+       /**
+        * Flushing this writer has no effect.
+        */
+       @Override
+       public void flush() {
+               // no-op
+       }
+
+       /**
+        * Writes a String to the {@link StringBuilder}.
+        *
+        * @param value The value to write
+        */
+       @Override
+       public void write(final String value) {
+               if (value != null) {
+                       builder.append(value);
+               }
+       }
+
+       /**
+        * Writes a portion of a character array to the {@link StringBuilder}.
+        *
+        * @param value  The value to write
+        * @param offset The index of the first character
+        * @param length The number of characters to write
+        */
+       @Override
+       public void write(final char[] value, final int offset, final int length) {
+               if (value != null) {
+                       builder.append(value, offset, length);
+               }
+       }
+
+       /**
+        * Returns the underlying builder.
+        *
+        * @return The underlying builder
+        */
+       public StringBuilder getBuilder() {
+               return builder;
+       }
+
+       /**
+        * Returns {@link StringBuilder#toString()}.
+        *
+        * @return The contents of the String builder.
+        */
+       @Override
+       public String toString() {
+               return builder.toString();
+       }
+
+}
\ No newline at end of file
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/FontFileReader.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/FontFileReader.java
new file mode 100644 (file)
index 0000000..d8a5207
--- /dev/null
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.truetype;
+
+import com.jaredrummler.fontreader.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Reads a TrueType font file into a byte array and
+ * provides file like functions for array access.
+ */
+public class FontFileReader {
+
+       private final int fsize; // file size
+       private int current;    // current position in file
+       private final byte[] file;
+
+       /**
+        * Constructor
+        *
+        * @param in InputStream to read from
+        * @throws IOException In case of an I/O problem
+        */
+       public FontFileReader(InputStream in) throws IOException {
+               this.file = IOUtils.toByteArray(in);
+               this.fsize = this.file.length;
+               this.current = 0;
+       }
+
+       /**
+        * Set current file position to offset
+        *
+        * @param offset The new offset to set
+        * @throws IOException In case of an I/O problem
+        */
+       public void seekSet(long offset) throws IOException {
+               if (offset > fsize || offset < 0) {
+                       throw new java.io.EOFException("Reached EOF, file size=" + fsize
+                                       + " offset=" + offset);
+               }
+               current = (int) offset;
+       }
+
+       /**
+        * Skip a given number of bytes.
+        *
+        * @param add The number of bytes to advance
+        * @throws IOException In case of an I/O problem
+        */
+       public void skip(long add) throws IOException {
+               seekSet(current + add);
+       }
+
+       /**
+        * Returns current file position.
+        *
+        * @return int The current position.
+        */
+       public int getCurrentPos() {
+               return current;
+       }
+
+       /**
+        * Returns the size of the file.
+        *
+        * @return int The filesize
+        */
+       public int getFileSize() {
+               return fsize;
+       }
+
+       /**
+        * Read 1 byte.
+        *
+        * @return One byte
+        * @throws IOException If EOF is reached
+        */
+       private byte read() throws IOException {
+               if (current >= fsize) {
+                       throw new java.io.EOFException("Reached EOF, file size=" + fsize);
+               }
+
+               final byte ret = file[current++];
+               return ret;
+       }
+
+       /**
+        * Read 1 signed byte.
+        *
+        * @return One byte
+        * @throws IOException If EOF is reached
+        */
+       public final byte readTTFByte() throws IOException {
+               return read();
+       }
+
+       /**
+        * Read 1 unsigned byte.
+        *
+        * @return One unsigned byte
+        * @throws IOException If EOF is reached
+        */
+       public final int readTTFUByte() throws IOException {
+               final byte buf = read();
+
+               if (buf < 0) {
+                       return (256 + buf);
+               } else {
+                       return buf;
+               }
+       }
+
+       /**
+        * Read 2 bytes signed.
+        *
+        * @return One signed short
+        * @throws IOException If EOF is reached
+        */
+       public final short readTTFShort() throws IOException {
+               final int ret = (readTTFUByte() << 8) + readTTFUByte();
+               final short sret = (short) ret;
+               return sret;
+       }
+
+       /**
+        * Read 2 bytes unsigned.
+        *
+        * @return One unsigned short
+        * @throws IOException If EOF is reached
+        */
+       public final int readTTFUShort() throws IOException {
+               final int ret = (readTTFUByte() << 8) + readTTFUByte();
+               return ret;
+       }
+
+       /**
+        * Write a USHort at a given position.
+        *
+        * @param pos The absolute position to write to
+        * @param val The value to write
+        * @throws IOException If EOF is reached
+        */
+       public final void writeTTFUShort(long pos, int val) throws IOException {
+               if ((pos + 2) > fsize) {
+                       throw new java.io.EOFException("Reached EOF");
+               }
+               final byte b1 = (byte) ((val >> 8) & 0xff);
+               final byte b2 = (byte) (val & 0xff);
+               final int fileIndex = (int) pos;
+               file[fileIndex] = b1;
+               file[fileIndex + 1] = b2;
+       }
+
+       /**
+        * Read 2 bytes signed at position pos without changing current position.
+        *
+        * @param pos The absolute position to read from
+        * @return One signed short
+        * @throws IOException If EOF is reached
+        */
+       public final short readTTFShort(long pos) throws IOException {
+               final long cp = getCurrentPos();
+               seekSet(pos);
+               final short ret = readTTFShort();
+               seekSet(cp);
+               return ret;
+       }
+
+       /**
+        * Read 2 bytes unsigned at position pos without changing current position.
+        *
+        * @param pos The absolute position to read from
+        * @return One unsigned short
+        * @throws IOException If EOF is reached
+        */
+       public final int readTTFUShort(long pos) throws IOException {
+               long cp = getCurrentPos();
+               seekSet(pos);
+               int ret = readTTFUShort();
+               seekSet(cp);
+               return ret;
+       }
+
+       /**
+        * Read 4 bytes.
+        *
+        * @return One signed integer
+        * @throws IOException If EOF is reached
+        */
+       public final int readTTFLong() throws IOException {
+               long ret = readTTFUByte();    // << 8;
+               ret = (ret << 8) + readTTFUByte();
+               ret = (ret << 8) + readTTFUByte();
+               ret = (ret << 8) + readTTFUByte();
+
+               return (int) ret;
+       }
+
+       /**
+        * Read 4 bytes.
+        *
+        * @return One unsigned integer
+        * @throws IOException If EOF is reached
+        */
+       public final long readTTFULong() throws IOException {
+               long ret = readTTFUByte();
+               ret = (ret << 8) + readTTFUByte();
+               ret = (ret << 8) + readTTFUByte();
+               ret = (ret << 8) + readTTFUByte();
+
+               return ret;
+       }
+
+       /**
+        * Read a NUL terminated ISO-8859-1 string.
+        *
+        * @return A String
+        * @throws IOException If EOF is reached
+        */
+       public final String readTTFString() throws IOException {
+               int i = current;
+               while (file[i++] != 0) {
+                       if (i >= fsize) {
+                               throw new java.io.EOFException("Reached EOF, file size="
+                                               + fsize);
+                       }
+               }
+
+               byte[] tmp = new byte[i - current - 1];
+               System.arraycopy(file, current, tmp, 0, i - current - 1);
+               return new String(tmp, "ISO-8859-1");
+       }
+
+       /**
+        * Read an ISO-8859-1 string of len bytes.
+        *
+        * @param len The length of the string to read
+        * @return A String
+        * @throws IOException If EOF is reached
+        */
+       public final String readTTFString(int len) throws IOException {
+               if ((len + current) > fsize) {
+                       throw new java.io.EOFException("Reached EOF, file size=" + fsize);
+               }
+
+               byte[] tmp = new byte[len];
+               System.arraycopy(file, current, tmp, 0, len);
+               current += len;
+               final String encoding;
+               if ((tmp.length > 0) && (tmp[0] == 0)) {
+                       encoding = "UTF-16BE";
+               } else {
+                       encoding = "ISO-8859-1";
+               }
+               return new String(tmp, encoding);
+       }
+
+       /**
+        * Read an ISO-8859-1 string of len bytes.
+        *
+        * @param len        The length of the string to read
+        * @param encodingID the string encoding id (presently ignored; always uses UTF-16BE)
+        * @return A String
+        * @throws IOException If EOF is reached
+        */
+       public final String readTTFString(int len, int encodingID) throws IOException {
+               if ((len + current) > fsize) {
+                       throw new java.io.EOFException("Reached EOF, file size=" + fsize);
+               }
+
+               byte[] tmp = new byte[len];
+               System.arraycopy(file, current, tmp, 0, len);
+               current += len;
+               final String encoding;
+               encoding = "UTF-16BE"; //Use this for all known encoding IDs for now
+               return new String(tmp, encoding);
+       }
+
+       /**
+        * Return a copy of the internal array
+        *
+        * @param offset The absolute offset to start reading from
+        * @param length The number of bytes to read
+        * @return An array of bytes
+        * @throws IOException if out of bounds
+        */
+       public byte[] getBytes(int offset,
+                                                  int length) throws IOException {
+               if ((offset + length) > fsize) {
+                       throw new IOException("Reached EOF");
+               }
+
+               byte[] ret = new byte[length];
+               System.arraycopy(file, offset, ret, 0, length);
+               return ret;
+       }
+
+       /**
+        * Returns the full byte array representation of the file.
+        *
+        * @return byte array.
+        */
+       public byte[] getAllBytes() {
+               return file;
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/GlyphTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/GlyphTable.java
new file mode 100644 (file)
index 0000000..50d6c2a
--- /dev/null
@@ -0,0 +1,1442 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.truetype;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.*;
+import com.jaredrummler.fontreader.fonts.*;
+import com.jaredrummler.fontreader.util.GlyphSequence;
+import com.jaredrummler.fontreader.util.ScriptContextTester;
+
+import java.util.*;
+
+/**
+ * <p>Base class for all advanced typographic glyph tables.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GlyphTable {
+
+       /**
+        * substitution glyph table type
+        */
+       public static final int GLYPH_TABLE_TYPE_SUBSTITUTION = 1;
+       /**
+        * positioning glyph table type
+        */
+       public static final int GLYPH_TABLE_TYPE_POSITIONING = 2;
+       /**
+        * justification glyph table type
+        */
+       public static final int GLYPH_TABLE_TYPE_JUSTIFICATION = 3;
+       /**
+        * baseline glyph table type
+        */
+       public static final int GLYPH_TABLE_TYPE_BASELINE = 4;
+       /**
+        * definition glyph table type
+        */
+       public static final int GLYPH_TABLE_TYPE_DEFINITION = 5;
+
+       // (optional) glyph definition table in table types other than glyph definition table
+       private GlyphTable gdef;
+
+       // map from lookup specs to lists of strings, each of which identifies a lookup table (consisting of one or more subtables)
+       private Map<LookupSpec, List<String>> lookups;
+
+       // map from lookup identifiers to lookup tables
+       private Map<String, LookupTable> lookupTables;
+
+       // cache for lookups matching
+       private Map<LookupSpec, Map<LookupSpec, List<LookupTable>>> matchedLookups;
+
+       // if true, then prevent further subtable addition
+       private boolean frozen;
+
+       /**
+        * Instantiate glyph table with specified lookups.
+        *
+        * @param gdef    glyph definition table that applies
+        * @param lookups map from lookup specs to lookup tables
+        */
+       public GlyphTable(GlyphTable gdef, Map<LookupSpec, List<String>> lookups) {
+               if ((gdef != null) && !(gdef instanceof GlyphDefinitionTable)) {
+                       throw new AdvancedTypographicTableFormatException("bad glyph definition table");
+               } else if (lookups == null) {
+                       throw new AdvancedTypographicTableFormatException("lookups must be non-null map");
+               } else {
+                       this.gdef = gdef;
+                       this.lookups = lookups;
+                       this.lookupTables = new LinkedHashMap<>();
+                       this.matchedLookups = new HashMap<>();
+               }
+       }
+
+       /**
+        * Obtain glyph definition table.
+        *
+        * @return (possibly null) glyph definition table
+        */
+       public GlyphDefinitionTable getGlyphDefinitions() {
+               return (GlyphDefinitionTable) gdef;
+       }
+
+       /**
+        * Obtain list of all lookup specifications.
+        *
+        * @return (possibly empty) list of all lookup specifications
+        */
+       public List<LookupSpec> getLookups() {
+               return matchLookupSpecs("*", "*", "*");
+       }
+
+       /**
+        * Obtain ordered list of all lookup tables, where order is by lookup identifier, which
+        * lexicographic ordering follows the lookup list order.
+        *
+        * @return (possibly empty) ordered list of all lookup tables
+        */
+       public List<LookupTable> getLookupTables() {
+               TreeSet<String> lids = new TreeSet<>(lookupTables.keySet());
+               List<LookupTable> ltl = new ArrayList<>(lids.size());
+               for (String lid : lids) {
+                       ltl.add(lookupTables.get(lid));
+               }
+               return ltl;
+       }
+
+       /**
+        * Obtain lookup table by lookup id. This method is used by test code, and provides
+        * access to embedded lookups not normally accessed by {script, language, feature} lookup spec.
+        *
+        * @param lid lookup id
+        * @return table associated with lookup id or null if none
+        */
+       public LookupTable getLookupTable(String lid) {
+               return lookupTables.get(lid);
+       }
+
+       /**
+        * Add a subtable.
+        *
+        * @param subtable a (non-null) glyph subtable
+        */
+       protected void addSubtable(GlyphSubtable subtable) {
+               // ensure table is not frozen
+               if (frozen) {
+                       throw new IllegalStateException("glyph table is frozen, subtable addition prohibited");
+               }
+               // set subtable's table reference to this table
+               subtable.setTable(this);
+               // add subtable to this table's subtable collection
+               String lid = subtable.getLookupId();
+               if (lookupTables.containsKey(lid)) {
+                       LookupTable lt = lookupTables.get(lid);
+                       lt.addSubtable(subtable);
+               } else {
+                       LookupTable lt = new LookupTable(lid, subtable);
+                       lookupTables.put(lid, lt);
+               }
+       }
+
+       /**
+        * Freeze subtables, i.e., do not allow further subtable addition, and
+        * create resulting cached state.
+        */
+       protected void freezeSubtables() {
+               if (!frozen) {
+                       for (LookupTable lt : lookupTables.values()) {
+                               lt.freezeSubtables(lookupTables);
+                       }
+                       frozen = true;
+               }
+       }
+
+       /**
+        * Match lookup specifications according to <script,language,feature> tuple, where
+        * '*' is a wildcard for a tuple component.
+        *
+        * @param script   a script identifier
+        * @param language a language identifier
+        * @param feature  a feature identifier
+        * @return a (possibly empty) array of matching lookup specifications
+        */
+       public List<LookupSpec> matchLookupSpecs(String script, String language, String feature) {
+               Set<LookupSpec> keys = lookups.keySet();
+               List<LookupSpec> matches = new ArrayList<>();
+               for (LookupSpec ls : keys) {
+                       if (!"*".equals(script)) {
+                               if (!ls.getScript().equals(script)) {
+                                       continue;
+                               }
+                       }
+                       if (!"*".equals(language)) {
+                               if (!ls.getLanguage().equals(language)) {
+                                       continue;
+                               }
+                       }
+                       if (!"*".equals(feature)) {
+                               if (!ls.getFeature().equals(feature)) {
+                                       continue;
+                               }
+                       }
+                       matches.add(ls);
+               }
+               return matches;
+       }
+
+       /**
+        * Match lookup specifications according to <script,language,feature> tuple, where
+        * '*' is a wildcard for a tuple component.
+        *
+        * @param script   a script identifier
+        * @param language a language identifier
+        * @param feature  a feature identifier
+        * @return a (possibly empty) map from matching lookup specifications to lists of corresponding lookup tables
+        */
+       public Map<LookupSpec, List<LookupTable>> matchLookups(String script, String language, String feature) {
+               LookupSpec lsm = new LookupSpec(script, language, feature, true, true);
+               Map<LookupSpec, List<LookupTable>> lm = matchedLookups.get(lsm);
+               if (lm == null) {
+                       lm = new LinkedHashMap<>();
+                       List<LookupSpec> lsl = matchLookupSpecs(script, language, feature);
+                       for (LookupSpec ls : lsl) {
+                               lm.put(ls, findLookupTables(ls));
+                       }
+                       matchedLookups.put(lsm, lm);
+               }
+               if (lm.isEmpty() && !OTFScript.isDefault(script) && !OTFScript.isWildCard(script)) {
+                       return matchLookups(OTFScript.DEFAULT, OTFLanguage.DEFAULT, feature);
+               } else {
+                       return lm;
+               }
+       }
+
+       /**
+        * Obtain ordered list of glyph lookup tables that match a specific lookup specification.
+        *
+        * @param ls a (non-null) lookup specification
+        * @return a (possibly empty) ordered list of lookup tables whose corresponding lookup specifications match the
+        * specified lookup spec
+        */
+       public List<LookupTable> findLookupTables(LookupSpec ls) {
+               TreeSet<LookupTable> lts = new TreeSet<>();
+               List<String> ids;
+               if ((ids = lookups.get(ls)) != null) {
+                       for (String lid : ids) {
+                               LookupTable lt;
+                               if ((lt = lookupTables.get(lid)) != null) {
+                                       lts.add(lt);
+                               }
+                       }
+               }
+               return new ArrayList<>(lts);
+       }
+
+       /**
+        * Assemble ordered array of lookup table use specifications according to the specified features and candidate
+        * lookups,
+        * where the order of the array is in accordance to the order of the applicable lookup list.
+        *
+        * @param features array of feature identifiers to apply
+        * @param lookups  a mapping from lookup specifications to lists of look tables from which to select lookup tables according to
+        *                 the specified features
+        * @return ordered array of assembled lookup table use specifications
+        */
+       public UseSpec[] assembleLookups(String[] features, Map<LookupSpec, List<LookupTable>> lookups) {
+               TreeSet<UseSpec> uss = new TreeSet<UseSpec>();
+               for (String feature : features) {
+                       for (Map.Entry<LookupSpec, List<LookupTable>> e : lookups.entrySet()) {
+                               LookupSpec ls = e.getKey();
+                               if (ls.getFeature().equals(feature)) {
+                                       List<LookupTable> ltl = e.getValue();
+                                       if (ltl != null) {
+                                               for (LookupTable lt : ltl) {
+                                                       uss.add(new UseSpec(lt, feature));
+                                               }
+                                       }
+                               }
+                       }
+               }
+               return uss.toArray(new UseSpec[uss.size()]);
+       }
+
+       /**
+        * Determine if table supports specific feature, i.e., supports at least one lookup.
+        *
+        * @param script   to qualify feature lookup
+        * @param language to qualify feature lookup
+        * @param feature  to test
+        * @return true if feature supported (has at least one lookup)
+        */
+       public boolean hasFeature(String script, String language, String feature) {
+               UseSpec[] usa = assembleLookups(new String[]{feature}, matchLookups(script, language, feature));
+               return usa.length > 0;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String toString() {
+               StringBuffer sb = new StringBuffer(super.toString());
+               sb.append("{");
+               sb.append("lookups={");
+               sb.append(lookups.toString());
+               sb.append("},lookupTables={");
+               sb.append(lookupTables.toString());
+               sb.append("}}");
+               return sb.toString();
+       }
+
+       /**
+        * Obtain glyph table type from name.
+        *
+        * @param name of table type to map to type value
+        * @return glyph table type (as an integer constant)
+        */
+       public static int getTableTypeFromName(String name) {
+               int t;
+               String s = name.toLowerCase();
+               if ("gsub".equals(s)) {
+                       t = GLYPH_TABLE_TYPE_SUBSTITUTION;
+               } else if ("gpos".equals(s)) {
+                       t = GLYPH_TABLE_TYPE_POSITIONING;
+               } else if ("jstf".equals(s)) {
+                       t = GLYPH_TABLE_TYPE_JUSTIFICATION;
+               } else if ("base".equals(s)) {
+                       t = GLYPH_TABLE_TYPE_BASELINE;
+               } else if ("gdef".equals(s)) {
+                       t = GLYPH_TABLE_TYPE_DEFINITION;
+               } else {
+                       t = -1;
+               }
+               return t;
+       }
+
+       /**
+        * Resolve references to lookup tables in a collection of rules sets.
+        *
+        * @param rsa          array of rule sets
+        * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+        */
+       public static void resolveLookupReferences(RuleSet[] rsa, Map<String, LookupTable> lookupTables) {
+               if ((rsa != null) && (lookupTables != null)) {
+                       for (RuleSet rs : rsa) {
+                               if (rs != null) {
+                                       rs.resolveLookupReferences(lookupTables);
+                               }
+                       }
+               }
+       }
+
+       /**
+        * A structure class encapsulating a lookup specification as a <script,language,feature> tuple.
+        */
+       public static class LookupSpec implements Comparable {
+
+               private final String script;
+               private final String language;
+               private final String feature;
+
+               /**
+                * Instantiate lookup spec.
+                *
+                * @param script   a script identifier
+                * @param language a language identifier
+                * @param feature  a feature identifier
+                */
+               public LookupSpec(String script, String language, String feature) {
+                       this(script, language, feature, false, false);
+               }
+
+               /**
+                * Instantiate lookup spec.
+                *
+                * @param script         a script identifier
+                * @param language       a language identifier
+                * @param feature        a feature identifier
+                * @param permitEmpty    if true then permit empty script, language, or feature
+                * @param permitWildcard if true then permit wildcard script, language, or feature
+                */
+               LookupSpec(String script, String language, String feature, boolean permitEmpty, boolean permitWildcard) {
+                       if ((script == null) || (!permitEmpty && (script.length() == 0))) {
+                               throw new AdvancedTypographicTableFormatException("script must be non-empty string");
+                       } else if ((language == null) || (!permitEmpty && (language.length() == 0))) {
+                               throw new AdvancedTypographicTableFormatException("language must be non-empty string");
+                       } else if ((feature == null) || (!permitEmpty && (feature.length() == 0))) {
+                               throw new AdvancedTypographicTableFormatException("feature must be non-empty string");
+                       } else if (!permitWildcard && script.equals("*")) {
+                               throw new AdvancedTypographicTableFormatException("script must not be wildcard");
+                       } else if (!permitWildcard && language.equals("*")) {
+                               throw new AdvancedTypographicTableFormatException("language must not be wildcard");
+                       } else if (!permitWildcard && feature.equals("*")) {
+                               throw new AdvancedTypographicTableFormatException("feature must not be wildcard");
+                       }
+                       this.script = script.trim();
+                       this.language = language.trim();
+                       this.feature = feature.trim();
+               }
+
+               /**
+                * @return script identifier
+                */
+               public String getScript() {
+                       return script;
+               }
+
+               /**
+                * @return language identifier
+                */
+               public String getLanguage() {
+                       return language;
+               }
+
+               /**
+                * @return feature identifier
+                */
+               public String getFeature() {
+                       return feature;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int hashCode() {
+                       int hc = 0;
+                       hc = 7 * hc + (hc ^ script.hashCode());
+                       hc = 11 * hc + (hc ^ language.hashCode());
+                       hc = 17 * hc + (hc ^ feature.hashCode());
+                       return hc;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean equals(Object o) {
+                       if (o instanceof LookupSpec) {
+                               LookupSpec l = (LookupSpec) o;
+                               if (!l.script.equals(script)) {
+                                       return false;
+                               } else if (!l.language.equals(language)) {
+                                       return false;
+                               } else {
+                                       return l.feature.equals(feature);
+                               }
+                       } else {
+                               return false;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int compareTo(Object o) {
+                       int d;
+                       if (o instanceof LookupSpec) {
+                               LookupSpec ls = (LookupSpec) o;
+                               if ((d = script.compareTo(ls.script)) == 0) {
+                                       if ((d = language.compareTo(ls.language)) == 0) {
+                                               if ((d = feature.compareTo(ls.feature)) == 0) {
+                                                       d = 0;
+                                               }
+                                       }
+                               }
+                       } else {
+                               d = -1;
+                       }
+                       return d;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer(super.toString());
+                       sb.append("{");
+                       sb.append("<'" + script + "'");
+                       sb.append(",'" + language + "'");
+                       sb.append(",'" + feature + "'");
+                       sb.append(">}");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>LookupTable</code> class comprising an identifier and an ordered list
+        * of glyph subtables, each of which employ the same lookup identifier.
+        */
+       public static class LookupTable implements Comparable {
+
+               private final String id;                                // lookup identifier
+               private final int idOrdinal;                            // parsed lookup identifier ordinal
+               private final List<GlyphSubtable> subtables;            // list of subtables
+               private boolean doesSub;                                // performs substitutions
+               private boolean doesPos;                                // performs positioning
+               private boolean frozen;                                 // if true, then don't permit further subtable additions
+               // frozen state
+               private GlyphSubtable[] subtablesArray;
+               private static GlyphSubtable[] subtablesArrayEmpty = new GlyphSubtable[0];
+
+               /**
+                * Instantiate a LookupTable.
+                *
+                * @param id       the lookup table's identifier
+                * @param subtable an initial subtable (or null)
+                */
+               public LookupTable(String id, GlyphSubtable subtable) {
+                       this(id, makeSingleton(subtable));
+               }
+
+               /**
+                * Instantiate a LookupTable.
+                *
+                * @param id        the lookup table's identifier
+                * @param subtables a pre-poplated list of subtables or null
+                */
+               public LookupTable(String id, List<GlyphSubtable> subtables) {
+                       this.id = id;
+                       this.idOrdinal = Integer.parseInt(id.substring(2));
+                       this.subtables = new LinkedList<GlyphSubtable>();
+                       if (subtables != null) {
+                               for (GlyphSubtable st : subtables) {
+                                       addSubtable(st);
+                               }
+                       }
+               }
+
+               /**
+                * @return the subtables as an array
+                */
+               public GlyphSubtable[] getSubtables() {
+                       if (frozen) {
+                               return (subtablesArray != null) ? subtablesArray : subtablesArrayEmpty;
+                       } else {
+                               if (doesSub) {
+                                       return subtables.toArray(new GlyphSubstitutionSubtable[subtables.size()]);
+                               } else if (doesPos) {
+                                       return subtables.toArray(new GlyphPositioningSubtable[subtables.size()]);
+                               } else {
+                                       return null;
+                               }
+                       }
+               }
+
+               /**
+                * Add a subtable into this lookup table's collecion of subtables according to its
+                * natural order.
+                *
+                * @param subtable to add
+                * @return true if subtable was not already present, otherwise false
+                */
+               public boolean addSubtable(GlyphSubtable subtable) {
+                       boolean added = false;
+                       // ensure table is not frozen
+                       if (frozen) {
+                               throw new IllegalStateException("glyph table is frozen, subtable addition prohibited");
+                       }
+                       // validate subtable to ensure consistency with current subtables
+                       validateSubtable(subtable);
+                       // insert subtable into ordered list
+                       for (ListIterator<GlyphSubtable> lit = subtables.listIterator(0); lit.hasNext(); ) {
+                               GlyphSubtable st = lit.next();
+                               int d;
+                               if ((d = subtable.compareTo(st)) < 0) {
+                                       // insert within list
+                                       lit.set(subtable);
+                                       lit.add(st);
+                                       added = true;
+                               } else if (d == 0) {
+                                       // duplicate entry is ignored
+                                       added = false;
+                                       subtable = null;
+                               }
+                       }
+                       // append at end of list
+                       if (!added && (subtable != null)) {
+                               subtables.add(subtable);
+                               added = true;
+                       }
+                       return added;
+               }
+
+               private void validateSubtable(GlyphSubtable subtable) {
+                       if (subtable == null) {
+                               throw new AdvancedTypographicTableFormatException("subtable must be non-null");
+                       }
+                       if (subtable instanceof GlyphSubstitutionSubtable) {
+                               if (doesPos) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "subtable must be positioning subtable, but is: " + subtable);
+                               } else {
+                                       doesSub = true;
+                               }
+                       }
+                       if (subtable instanceof GlyphPositioningSubtable) {
+                               if (doesSub) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "subtable must be substitution subtable, but is: " + subtable);
+                               } else {
+                                       doesPos = true;
+                               }
+                       }
+                       if (subtables.size() > 0) {
+                               GlyphSubtable st = subtables.get(0);
+                               if (!st.isCompatible(subtable)) {
+                                       throw new AdvancedTypographicTableFormatException(
+                                                       "subtable " + subtable + " is not compatible with subtable " + st);
+                               }
+                       }
+               }
+
+               /**
+                * Freeze subtables, i.e., do not allow further subtable addition, and
+                * create resulting cached state. In addition, resolve any references to
+                * lookup tables that appear in this lookup table's subtables.
+                *
+                * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+                */
+               public void freezeSubtables(Map<String, LookupTable> lookupTables) {
+                       if (!frozen) {
+                               GlyphSubtable[] sta = getSubtables();
+                               resolveLookupReferences(sta, lookupTables);
+                               this.subtablesArray = sta;
+                               this.frozen = true;
+                       }
+               }
+
+               private void resolveLookupReferences(GlyphSubtable[] subtables, Map<String, LookupTable> lookupTables) {
+                       if (subtables != null) {
+                               for (GlyphSubtable st : subtables) {
+                                       if (st != null) {
+                                               st.resolveLookupReferences(lookupTables);
+                                       }
+                               }
+                       }
+               }
+
+               /**
+                * Determine if this glyph table performs substitution.
+                *
+                * @return true if it performs substitution
+                */
+               public boolean performsSubstitution() {
+                       return doesSub;
+               }
+
+               /**
+                * Perform substitution processing using this lookup table's subtables.
+                *
+                * @param gs       an input glyph sequence
+                * @param script   a script identifier
+                * @param language a language identifier
+                * @param feature  a feature identifier
+                * @param sct      a script specific context tester (or null)
+                * @return the substituted (output) glyph sequence
+                */
+               public GlyphSequence substitute(GlyphSequence gs, String script, String language, String feature,
+                                                                               ScriptContextTester sct) {
+                       if (performsSubstitution()) {
+                               return GlyphSubstitutionSubtable
+                                               .substitute(gs, script, language, feature, (GlyphSubstitutionSubtable[]) subtablesArray, sct);
+                       } else {
+                               return gs;
+                       }
+               }
+
+               /**
+                * Perform substitution processing on an existing glyph substitution state object using this lookup table's
+                * subtables.
+                *
+                * @param ss            a glyph substitution state object
+                * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
+                * @return the substituted (output) glyph sequence
+                */
+               public GlyphSequence substitute(GlyphSubstitutionState ss, int sequenceIndex) {
+                       if (performsSubstitution()) {
+                               return GlyphSubstitutionSubtable.substitute(ss, (GlyphSubstitutionSubtable[]) subtablesArray, sequenceIndex);
+                       } else {
+                               return ss.getInput();
+                       }
+               }
+
+               /**
+                * Determine if this glyph table performs positioning.
+                *
+                * @return true if it performs positioning
+                */
+               public boolean performsPositioning() {
+                       return doesPos;
+               }
+
+               /**
+                * Perform positioning processing using this lookup table's subtables.
+                *
+                * @param gs          an input glyph sequence
+                * @param script      a script identifier
+                * @param language    a language identifier
+                * @param feature     a feature identifier
+                * @param fontSize    size in device units
+                * @param widths      array of default advancements for each glyph in font
+                * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments,
+                *                    in
+                *                    that order,
+                *                    with one 4-tuple for each element of glyph sequence
+                * @param sct         a script specific context tester (or null)
+                * @return true if some adjustment is not zero; otherwise, false
+                */
+               public boolean position(GlyphSequence gs, String script, String language, String feature, int fontSize,
+                                                               int[] widths, int[][] adjustments, ScriptContextTester sct) {
+                       return performsPositioning() && GlyphPositioningSubtable.position(gs, script, language, feature,
+                                       fontSize, (GlyphPositioningSubtable[]) subtablesArray, widths, adjustments, sct);
+               }
+
+               /**
+                * Perform positioning processing on an existing glyph positioning state object using this lookup table's
+                * subtables.
+                *
+                * @param ps            a glyph positioning state object
+                * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
+                * @return true if some adjustment is not zero; otherwise, false
+                */
+               public boolean position(GlyphPositioningState ps, int sequenceIndex) {
+                       return performsPositioning() &&
+                                       GlyphPositioningSubtable.position(ps, (GlyphPositioningSubtable[]) subtablesArray, sequenceIndex);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int hashCode() {
+                       return idOrdinal;
+               }
+
+               /**
+                * {@inheritDoc}
+                *
+                * @return true if identifier of the specified lookup table is the same
+                * as the identifier of this lookup table
+                */
+               public boolean equals(Object o) {
+                       if (o instanceof LookupTable) {
+                               LookupTable lt = (LookupTable) o;
+                               return idOrdinal == lt.idOrdinal;
+                       } else {
+                               return false;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                *
+                * @return the result of comparing the identifier of the specified lookup table with
+                * the identifier of this lookup table; lookup table identifiers take the form
+                * "lu(DIGIT)+", with comparison based on numerical ordering of numbers expressed by
+                * (DIGIT)+.
+                */
+               public int compareTo(Object o) {
+                       if (o instanceof LookupTable) {
+                               LookupTable lt = (LookupTable) o;
+                               int i = idOrdinal;
+                               int j = lt.idOrdinal;
+                               if (i < j) {
+                                       return -1;
+                               } else if (i > j) {
+                                       return 1;
+                               } else {
+                                       return 0;
+                               }
+                       } else {
+                               return -1;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{ ");
+                       sb.append("id = " + id);
+                       sb.append(", subtables = " + subtables);
+                       sb.append(" }");
+                       return sb.toString();
+               }
+
+               private static List<GlyphSubtable> makeSingleton(GlyphSubtable subtable) {
+                       if (subtable == null) {
+                               return null;
+                       } else {
+                               List<GlyphSubtable> stl = new ArrayList<GlyphSubtable>(1);
+                               stl.add(subtable);
+                               return stl;
+                       }
+               }
+
+       }
+
+       /**
+        * The <code>UseSpec</code> class comprises a lookup table reference
+        * and the feature that selected the lookup table.
+        */
+       public static class UseSpec implements Comparable {
+
+               /**
+                * lookup table to apply
+                */
+               private final LookupTable lookupTable;
+               /**
+                * feature that caused selection of the lookup table
+                */
+               private final String feature;
+
+               /**
+                * Construct a glyph lookup table use specification.
+                *
+                * @param lookupTable a glyph lookup table
+                * @param feature     a feature that caused lookup table selection
+                */
+               public UseSpec(LookupTable lookupTable, String feature) {
+                       this.lookupTable = lookupTable;
+                       this.feature = feature;
+               }
+
+               /**
+                * @return the lookup table
+                */
+               public LookupTable getLookupTable() {
+                       return lookupTable;
+               }
+
+               /**
+                * @return the feature that selected this lookup table
+                */
+               public String getFeature() {
+                       return feature;
+               }
+
+               /**
+                * Perform substitution processing using this use specification's lookup table.
+                *
+                * @param gs       an input glyph sequence
+                * @param script   a script identifier
+                * @param language a language identifier
+                * @param sct      a script specific context tester (or null)
+                * @return the substituted (output) glyph sequence
+                */
+               public GlyphSequence substitute(GlyphSequence gs, String script, String language, ScriptContextTester sct) {
+                       return lookupTable.substitute(gs, script, language, feature, sct);
+               }
+
+               /**
+                * Perform positioning processing using this use specification's lookup table.
+                *
+                * @param gs          an input glyph sequence
+                * @param script      a script identifier
+                * @param language    a language identifier
+                * @param fontSize    size in device units
+                * @param widths      array of default advancements for each glyph in font
+                * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments,
+                *                    in
+                *                    that order,
+                *                    with one 4-tuple for each element of glyph sequence
+                * @param sct         a script specific context tester (or null)
+                * @return true if some adjustment is not zero; otherwise, false
+                */
+               public boolean position(GlyphSequence gs, String script, String language, int fontSize, int[] widths,
+                                                               int[][] adjustments, ScriptContextTester sct) {
+                       return lookupTable.position(gs, script, language, feature, fontSize, widths, adjustments, sct);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int hashCode() {
+                       return lookupTable.hashCode();
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean equals(Object o) {
+                       if (o instanceof UseSpec) {
+                               UseSpec u = (UseSpec) o;
+                               return lookupTable.equals(u.lookupTable);
+                       } else {
+                               return false;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int compareTo(Object o) {
+                       if (o instanceof UseSpec) {
+                               UseSpec u = (UseSpec) o;
+                               return lookupTable.compareTo(u.lookupTable);
+                       } else {
+                               return -1;
+                       }
+               }
+
+       }
+
+       /**
+        * The <code>RuleLookup</code> class implements a rule lookup record, comprising
+        * a glyph sequence index and a lookup table index (in an applicable lookup list).
+        */
+       public static class RuleLookup {
+
+               private final int sequenceIndex;                        // index into input glyph sequence
+               private final int lookupIndex;                          // lookup list index
+               private LookupTable lookup;                             // resolved lookup table
+
+               /**
+                * Instantiate a RuleLookup.
+                *
+                * @param sequenceIndex the index into the input sequence
+                * @param lookupIndex   the lookup table index
+                */
+               public RuleLookup(int sequenceIndex, int lookupIndex) {
+                       this.sequenceIndex = sequenceIndex;
+                       this.lookupIndex = lookupIndex;
+                       this.lookup = null;
+               }
+
+               /**
+                * @return the sequence index
+                */
+               public int getSequenceIndex() {
+                       return sequenceIndex;
+               }
+
+               /**
+                * @return the lookup index
+                */
+               public int getLookupIndex() {
+                       return lookupIndex;
+               }
+
+               /**
+                * @return the lookup table
+                */
+               public LookupTable getLookup() {
+                       return lookup;
+               }
+
+               /**
+                * Resolve references to lookup tables.
+                *
+                * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+                */
+               public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
+                       if (lookupTables != null) {
+                               String lid = "lu" + Integer.toString(lookupIndex);
+                               LookupTable lt = (LookupTable) lookupTables.get(lid);
+                               if (lt != null) {
+                                       this.lookup = lt;
+                               }
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       return "{ sequenceIndex = " + sequenceIndex + ", lookupIndex = " + lookupIndex + " }";
+               }
+
+       }
+
+       /**
+        * The <code>Rule</code> class implements an array of rule lookup records.
+        */
+       public abstract static class Rule {
+
+               private final RuleLookup[] lookups;                     // rule lookups
+               private final int inputSequenceLength;                  // input sequence length
+
+               /**
+                * Instantiate a Rule.
+                *
+                * @param lookups             the rule's lookups
+                * @param inputSequenceLength the number of glyphs in the input sequence for this rule
+                */
+               protected Rule(RuleLookup[] lookups, int inputSequenceLength) {
+                       assert lookups != null;
+                       this.lookups = lookups;
+                       this.inputSequenceLength = inputSequenceLength;
+               }
+
+               /**
+                * @return the lookups
+                */
+               public RuleLookup[] getLookups() {
+                       return lookups;
+               }
+
+               /**
+                * @return the input sequence length
+                */
+               public int getInputSequenceLength() {
+                       return inputSequenceLength;
+               }
+
+               /**
+                * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves.
+                *
+                * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+                */
+               public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
+                       if (lookups != null) {
+                               for (RuleLookup l : lookups) {
+                                       if (l != null) {
+                                               l.resolveLookupReferences(lookupTables);
+                                       }
+                               }
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       return "{ lookups = " + Arrays.toString(lookups) + ", inputSequenceLength = " + inputSequenceLength + " }";
+               }
+
+       }
+
+       /**
+        * The <code>GlyphSequenceRule</code> class implements a subclass of <code>Rule</code>
+        * that supports matching on a specific glyph sequence.
+        */
+       public static class GlyphSequenceRule extends Rule {
+
+               private final int[] glyphs;                             // glyphs
+
+               /**
+                * Instantiate a GlyphSequenceRule.
+                *
+                * @param lookups             the rule's lookups
+                * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+                * @param glyphs              the rule's glyph sequence to match, starting with second glyph in sequence
+                */
+               public GlyphSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] glyphs) {
+                       super(lookups, inputSequenceLength);
+                       assert glyphs != null;
+                       this.glyphs = glyphs;
+               }
+
+               /**
+                * Obtain glyphs. N.B. that this array starts with the second
+                * glyph of the input sequence.
+                *
+                * @return the glyphs
+                */
+               public int[] getGlyphs() {
+                       return glyphs;
+               }
+
+               /**
+                * Obtain glyphs augmented by specified first glyph entry.
+                *
+                * @param firstGlyph to fill in first glyph entry
+                * @return the glyphs augmented by first glyph
+                */
+               public int[] getGlyphs(int firstGlyph) {
+                       int[] ga = new int[glyphs.length + 1];
+                       ga[0] = firstGlyph;
+                       System.arraycopy(glyphs, 0, ga, 1, glyphs.length);
+                       return ga;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{ ");
+                       sb.append("lookups = " + Arrays.toString(getLookups()));
+                       sb.append(", glyphs = " + Arrays.toString(glyphs));
+                       sb.append(" }");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>ClassSequenceRule</code> class implements a subclass of <code>Rule</code>
+        * that supports matching on a specific glyph class sequence.
+        */
+       public static class ClassSequenceRule extends Rule {
+
+               private final int[] classes;                            // glyph classes
+
+               /**
+                * Instantiate a ClassSequenceRule.
+                *
+                * @param lookups             the rule's lookups
+                * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+                * @param classes             the rule's glyph class sequence to match, starting with second glyph in sequence
+                */
+               public ClassSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] classes) {
+                       super(lookups, inputSequenceLength);
+                       assert classes != null;
+                       this.classes = classes;
+               }
+
+               /**
+                * Obtain glyph classes. N.B. that this array starts with the class of the second
+                * glyph of the input sequence.
+                *
+                * @return the classes
+                */
+               public int[] getClasses() {
+                       return classes;
+               }
+
+               /**
+                * Obtain glyph classes augmented by specified first class entry.
+                *
+                * @param firstClass to fill in first class entry
+                * @return the classes augmented by first class
+                */
+               public int[] getClasses(int firstClass) {
+                       int[] ca = new int[classes.length + 1];
+                       ca[0] = firstClass;
+                       System.arraycopy(classes, 0, ca, 1, classes.length);
+                       return ca;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{ ");
+                       sb.append("lookups = " + Arrays.toString(getLookups()));
+                       sb.append(", classes = " + Arrays.toString(classes));
+                       sb.append(" }");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>CoverageSequenceRule</code> class implements a subclass of <code>Rule</code>
+        * that supports matching on a specific glyph coverage sequence.
+        */
+       public static class CoverageSequenceRule extends Rule {
+
+               private final GlyphCoverageTable[] coverages;           // glyph coverages
+
+               /**
+                * Instantiate a ClassSequenceRule.
+                *
+                * @param lookups             the rule's lookups
+                * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+                * @param coverages           the rule's glyph coverage sequence to match, starting with first glyph in sequence
+                */
+               public CoverageSequenceRule(RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages) {
+                       super(lookups, inputSequenceLength);
+                       assert coverages != null;
+                       this.coverages = coverages;
+               }
+
+               /**
+                * @return the coverages
+                */
+               public GlyphCoverageTable[] getCoverages() {
+                       return coverages;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{ ");
+                       sb.append("lookups = " + Arrays.toString(getLookups()));
+                       sb.append(", coverages = " + Arrays.toString(coverages));
+                       sb.append(" }");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>ChainedGlyphSequenceRule</code> class implements a subclass of <code>GlyphSequenceRule</code>
+        * that supports matching on a specific glyph sequence in a specific chained contextual.
+        */
+       public static class ChainedGlyphSequenceRule extends GlyphSequenceRule {
+
+               private final int[] backtrackGlyphs;                    // backtrack glyphs
+               private final int[] lookaheadGlyphs;                    // lookahead glyphs
+
+               /**
+                * Instantiate a ChainedGlyphSequenceRule.
+                *
+                * @param lookups             the rule's lookups
+                * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+                * @param glyphs              the rule's input glyph sequence to match, starting with second glyph in sequence
+                * @param backtrackGlyphs     the rule's backtrack glyph sequence to match, starting with first glyph in sequence
+                * @param lookaheadGlyphs     the rule's lookahead glyph sequence to match, starting with first glyph in sequence
+                */
+               public ChainedGlyphSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] glyphs, int[] backtrackGlyphs,
+                                                                               int[] lookaheadGlyphs) {
+                       super(lookups, inputSequenceLength, glyphs);
+                       assert backtrackGlyphs != null;
+                       assert lookaheadGlyphs != null;
+                       this.backtrackGlyphs = backtrackGlyphs;
+                       this.lookaheadGlyphs = lookaheadGlyphs;
+               }
+
+               /**
+                * @return the backtrack glyphs
+                */
+               public int[] getBacktrackGlyphs() {
+                       return backtrackGlyphs;
+               }
+
+               /**
+                * @return the lookahead glyphs
+                */
+               public int[] getLookaheadGlyphs() {
+                       return lookaheadGlyphs;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{ ");
+                       sb.append("lookups = " + Arrays.toString(getLookups()));
+                       sb.append(", glyphs = " + Arrays.toString(getGlyphs()));
+                       sb.append(", backtrackGlyphs = " + Arrays.toString(backtrackGlyphs));
+                       sb.append(", lookaheadGlyphs = " + Arrays.toString(lookaheadGlyphs));
+                       sb.append(" }");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>ChainedClassSequenceRule</code> class implements a subclass of <code>ClassSequenceRule</code>
+        * that supports matching on a specific glyph class sequence in a specific chained contextual.
+        */
+       public static class ChainedClassSequenceRule extends ClassSequenceRule {
+
+               private final int[] backtrackClasses;                    // backtrack classes
+               private final int[] lookaheadClasses;                    // lookahead classes
+
+               /**
+                * Instantiate a ChainedClassSequenceRule.
+                *
+                * @param lookups             the rule's lookups
+                * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+                * @param classes             the rule's input glyph class sequence to match, starting with second glyph in sequence
+                * @param backtrackClasses    the rule's backtrack glyph class sequence to match, starting with first glyph in sequence
+                * @param lookaheadClasses    the rule's lookahead glyph class sequence to match, starting with first glyph in sequence
+                */
+               public ChainedClassSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] classes,
+                                                                               int[] backtrackClasses, int[] lookaheadClasses) {
+                       super(lookups, inputSequenceLength, classes);
+                       assert backtrackClasses != null;
+                       assert lookaheadClasses != null;
+                       this.backtrackClasses = backtrackClasses;
+                       this.lookaheadClasses = lookaheadClasses;
+               }
+
+               /**
+                * @return the backtrack classes
+                */
+               public int[] getBacktrackClasses() {
+                       return backtrackClasses;
+               }
+
+               /**
+                * @return the lookahead classes
+                */
+               public int[] getLookaheadClasses() {
+                       return lookaheadClasses;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{ ");
+                       sb.append("lookups = " + Arrays.toString(getLookups()));
+                       sb.append(", classes = " + Arrays.toString(getClasses()));
+                       sb.append(", backtrackClasses = " + Arrays.toString(backtrackClasses));
+                       sb.append(", lookaheadClasses = " + Arrays.toString(lookaheadClasses));
+                       sb.append(" }");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>ChainedCoverageSequenceRule</code> class implements a subclass of <code>CoverageSequenceRule</code>
+        * that supports matching on a specific glyph class sequence in a specific chained contextual.
+        */
+       public static class ChainedCoverageSequenceRule extends CoverageSequenceRule {
+
+               private final GlyphCoverageTable[] backtrackCoverages;  // backtrack coverages
+               private final GlyphCoverageTable[] lookaheadCoverages;  // lookahead coverages
+
+               /**
+                * Instantiate a ChainedCoverageSequenceRule.
+                *
+                * @param lookups             the rule's lookups
+                * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+                * @param coverages           the rule's input glyph class sequence to match, starting with first glyph in sequence
+                * @param backtrackCoverages  the rule's backtrack glyph class sequence to match, starting with first glyph in sequence
+                * @param lookaheadCoverages  the rule's lookahead glyph class sequence to match, starting with first glyph in sequence
+                */
+               public ChainedCoverageSequenceRule(RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages,
+                                                                                  GlyphCoverageTable[] backtrackCoverages,
+                                                                                  GlyphCoverageTable[] lookaheadCoverages) {
+                       super(lookups, inputSequenceLength, coverages);
+                       assert backtrackCoverages != null;
+                       assert lookaheadCoverages != null;
+                       this.backtrackCoverages = backtrackCoverages;
+                       this.lookaheadCoverages = lookaheadCoverages;
+               }
+
+               /**
+                * @return the backtrack coverages
+                */
+               public GlyphCoverageTable[] getBacktrackCoverages() {
+                       return backtrackCoverages;
+               }
+
+               /**
+                * @return the lookahead coverages
+                */
+               public GlyphCoverageTable[] getLookaheadCoverages() {
+                       return lookaheadCoverages;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       StringBuffer sb = new StringBuffer();
+                       sb.append("{ ");
+                       sb.append("lookups = " + Arrays.toString(getLookups()));
+                       sb.append(", coverages = " + Arrays.toString(getCoverages()));
+                       sb.append(", backtrackCoverages = " + Arrays.toString(backtrackCoverages));
+                       sb.append(", lookaheadCoverages = " + Arrays.toString(lookaheadCoverages));
+                       sb.append(" }");
+                       return sb.toString();
+               }
+
+       }
+
+       /**
+        * The <code>RuleSet</code> class implements a collection of rules, which
+        * may or may not be the same rule type.
+        */
+       public static class RuleSet {
+
+               private final Rule[] rules;                             // set of rules
+
+               /**
+                * Instantiate a Rule Set.
+                *
+                * @param rules the rules
+                * @throws AdvancedTypographicTableFormatException if rules or some element of rules is null
+                */
+               public RuleSet(Rule[] rules) throws AdvancedTypographicTableFormatException {
+                       // enforce rules array instance
+                       if (rules == null) {
+                               throw new AdvancedTypographicTableFormatException("rules[] is null");
+                       }
+                       this.rules = rules;
+               }
+
+               /**
+                * @return the rules
+                */
+               public Rule[] getRules() {
+                       return rules;
+               }
+
+               /**
+                * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves.
+                *
+                * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+                */
+               public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
+                       if (rules != null) {
+                               for (Rule r : rules) {
+                                       if (r != null) {
+                                               r.resolveLookupReferences(lookupTables);
+                                       }
+                               }
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public String toString() {
+                       return "{ rules = " + Arrays.toString(rules) + " }";
+               }
+
+       }
+
+       /**
+        * The <code>HomogenousRuleSet</code> class implements a collection of rules, which
+        * must be the same rule type (i.e., same concrete rule class) or null.
+        */
+       public static class HomogeneousRuleSet extends RuleSet {
+
+               /**
+                * Instantiate a Homogeneous Rule Set.
+                *
+                * @param rules the rules
+                * @throws AdvancedTypographicTableFormatException if some rule[i] is not an instance of rule[0]
+                */
+               public HomogeneousRuleSet(Rule[] rules) throws AdvancedTypographicTableFormatException {
+                       super(rules);
+                       // find first non-null rule
+                       Rule r0 = null;
+                       for (int i = 1, n = rules.length; (r0 == null) && (i < n); i++) {
+                               if (rules[i] != null) {
+                                       r0 = rules[i];
+                               }
+                       }
+                       // enforce rule instance homogeneity
+                       if (r0 != null) {
+                               Class c = r0.getClass();
+                               for (int i = 1, n = rules.length; i < n; i++) {
+                                       Rule r = rules[i];
+                                       if ((r != null) && !c.isInstance(r)) {
+                                               throw new AdvancedTypographicTableFormatException("rules[" + i + "] is not an instance of " + c.getName());
+                                       }
+                               }
+                       }
+
+               }
+
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFDirTabEntry.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFDirTabEntry.java
new file mode 100644 (file)
index 0000000..cf1ad39
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.truetype;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+/**
+ * This class represents an entry to a TrueType font's Dir Tab.
+ */
+public class OFDirTabEntry {
+
+       private byte[] tag = new byte[4];
+       private long checksum;
+       private long offset;
+       private long length;
+
+       public OFDirTabEntry() {
+       }
+
+       public OFDirTabEntry(long offset, long length) {
+               this.offset = offset;
+               this.length = length;
+       }
+
+       /**
+        * Read Dir Tab.
+        *
+        * @param in font file reader
+        * @return tag name
+        * @throws IOException upon I/O exception
+        */
+       public String read(FontFileReader in) throws IOException {
+               tag[0] = in.readTTFByte();
+               tag[1] = in.readTTFByte();
+               tag[2] = in.readTTFByte();
+               tag[3] = in.readTTFByte();
+
+               checksum = in.readTTFLong();
+               offset = in.readTTFULong();
+               length = in.readTTFULong();
+
+               return getTagString();
+       }
+
+       @Override
+       public String toString() {
+               return "Read dir tab [" + Arrays.toString(tag) + "]"
+                               + " offset: " + offset
+                               + " length: " + length
+                               + " name: " + getTagString();
+       }
+
+       /**
+        * Returns the checksum.
+        *
+        * @return int
+        */
+       public long getChecksum() {
+               return checksum;
+       }
+
+       /**
+        * Returns the length.
+        *
+        * @return long
+        */
+       public long getLength() {
+               return length;
+       }
+
+       /**
+        * Returns the offset.
+        *
+        * @return long
+        */
+       public long getOffset() {
+               return offset;
+       }
+
+       /**
+        * Returns the tag bytes.
+        *
+        * @return byte[]
+        */
+       public byte[] getTag() {
+               return tag;
+       }
+
+       /**
+        * Returns the tag bytes.
+        *
+        * @return byte[]
+        */
+       public String getTagString() {
+               try {
+                       return new String(tag, "ISO-8859-1");
+               } catch (UnsupportedEncodingException e) {
+                       return this.toString(); // Should never happen.
+               }
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFMtxEntry.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFMtxEntry.java
new file mode 100644 (file)
index 0000000..0fc77b1
--- /dev/null
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.truetype;
+
+import java.util.List;
+
+/**
+ * This class represents a TrueType Mtx Entry.
+ */
+public class OFMtxEntry {
+
+       private int wx;
+       private int lsb;
+       private String name = "";
+       private int index;
+       private List unicodeIndex = new java.util.ArrayList();
+       private int[] boundingBox = new int[4];
+       private long offset;
+       private byte found;
+
+       /**
+        * Returns a String representation of this object.
+        *
+        * @param t TTFFile to use for unit conversion
+        * @return String String representation
+        */
+       public String toString(TTFFile t) {
+               return "Glyph " + name + " index: " + getIndexAsString() + " bbox ["
+                               + t.convertTTFUnit2PDFUnit(boundingBox[0]) + " "
+                               + t.convertTTFUnit2PDFUnit(boundingBox[1]) + " "
+                               + t.convertTTFUnit2PDFUnit(boundingBox[2]) + " "
+                               + t.convertTTFUnit2PDFUnit(boundingBox[3]) + "] wx: "
+                               + t.convertTTFUnit2PDFUnit(wx);
+       }
+
+       /**
+        * Returns the boundingBox.
+        *
+        * @return int[]
+        */
+       public int[] getBoundingBox() {
+               return boundingBox;
+       }
+
+       /**
+        * Sets the boundingBox.
+        *
+        * @param boundingBox The boundingBox to set
+        */
+       public void setBoundingBox(int[] boundingBox) {
+               this.boundingBox = boundingBox;
+       }
+
+       /**
+        * Returns the found.
+        *
+        * @return byte
+        */
+       public byte getFound() {
+               return found;
+       }
+
+       /**
+        * Returns the index.
+        *
+        * @return int
+        */
+       public int getIndex() {
+               return index;
+       }
+
+       /**
+        * Determines whether this index represents a reserved character.
+        *
+        * @return True if it is reserved
+        */
+       public boolean isIndexReserved() {
+               return (getIndex() >= 32768) && (getIndex() <= 65535);
+       }
+
+       /**
+        * Returns a String representation of the index taking into account if
+        * the index is in the reserved range.
+        *
+        * @return index as String
+        */
+       public String getIndexAsString() {
+               if (isIndexReserved()) {
+                       return Integer.toString(getIndex()) + " (reserved)";
+               } else {
+                       return Integer.toString(getIndex());
+               }
+       }
+
+       /**
+        * Returns the lsb.
+        *
+        * @return int
+        */
+       public int getLsb() {
+               return lsb;
+       }
+
+       /**
+        * Returns the name.
+        *
+        * @return String
+        */
+       public String getName() {
+               return name;
+       }
+
+       /**
+        * Returns the offset.
+        *
+        * @return long
+        */
+       public long getOffset() {
+               return offset;
+       }
+
+       /**
+        * Returns the unicodeIndex.
+        *
+        * @return List
+        */
+       public List getUnicodeIndex() {
+               return unicodeIndex;
+       }
+
+       /**
+        * Returns the wx.
+        *
+        * @return int
+        */
+       public int getWx() {
+               return wx;
+       }
+
+       /**
+        * Sets the found.
+        *
+        * @param found The found to set
+        */
+       public void setFound(byte found) {
+               this.found = found;
+       }
+
+       /**
+        * Sets the index.
+        *
+        * @param index The index to set
+        */
+       public void setIndex(int index) {
+               this.index = index;
+       }
+
+       /**
+        * Sets the lsb.
+        *
+        * @param lsb The lsb to set
+        */
+       public void setLsb(int lsb) {
+               this.lsb = lsb;
+       }
+
+       /**
+        * Sets the name.
+        *
+        * @param name The name to set
+        */
+       public void setName(String name) {
+               this.name = name;
+       }
+
+       /**
+        * Sets the offset.
+        *
+        * @param offset The offset to set
+        */
+       public void setOffset(long offset) {
+               this.offset = offset;
+       }
+
+       /**
+        * Sets the wx.
+        *
+        * @param wx The wx to set
+        */
+       public void setWx(int wx) {
+               this.wx = wx;
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFTableName.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFTableName.java
new file mode 100644 (file)
index 0000000..2172f0e
--- /dev/null
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.truetype;
+
+/**
+ * Represents table names as found in a TrueType font's Table Directory.
+ * TrueType fonts may have custom tables so we cannot use an enum.
+ */
+public final class OFTableName {
+
+       /**
+        * The first table in a TrueType font file containing metadata about other tables.
+        */
+       public static final OFTableName TABLE_DIRECTORY = new OFTableName("tableDirectory");
+
+       /**
+        * Baseline data
+        */
+       public static final OFTableName BASE = new OFTableName("BASE");
+
+       /**
+        * CFF data/
+        */
+       public static final OFTableName CFF = new OFTableName("CFF ");
+
+       /**
+        * Embedded bitmap data.
+        */
+       public static final OFTableName EBDT = new OFTableName("EBDT");
+
+       /**
+        * Embedded bitmap location data.
+        */
+       public static final OFTableName EBLC = new OFTableName("EBLC");
+
+       /**
+        * Embedded bitmap scaling data.
+        */
+       public static final OFTableName EBSC = new OFTableName("EBSC");
+
+       /**
+        * A FontForge specific table.
+        */
+       public static final OFTableName FFTM = new OFTableName("FFTM");
+
+       /**
+        * Divides glyphs into various classes that make using the GPOS/GSUB tables easier.
+        */
+       public static final OFTableName GDEF = new OFTableName("GDEF");
+
+       /**
+        * Provides kerning information, mark-to-base, etc. for opentype fonts.
+        */
+       public static final OFTableName GPOS = new OFTableName("GPOS");
+
+       /**
+        * Provides ligature information, swash, etc. for opentype fonts.
+        */
+       public static final OFTableName GSUB = new OFTableName("GSUB");
+
+       /**
+        * Linear threshold table.
+        */
+       public static final OFTableName LTSH = new OFTableName("LTSH");
+
+       /**
+        * OS/2 and Windows specific metrics.
+        */
+       public static final OFTableName OS2 = new OFTableName("OS/2");
+
+       /**
+        * PCL 5 data.
+        */
+       public static final OFTableName PCLT = new OFTableName("PCLT");
+
+       /**
+        * Vertical Device Metrics table.
+        */
+       public static final OFTableName VDMX = new OFTableName("VDMX");
+
+       /**
+        * Character to glyph mapping.
+        */
+       public static final OFTableName CMAP = new OFTableName("cmap");
+
+       /**
+        * Control Value Table.
+        */
+       public static final OFTableName CVT = new OFTableName("cvt ");
+
+       /**
+        * Font program.
+        */
+       public static final OFTableName FPGM = new OFTableName("fpgm");
+
+       /**
+        * Grid-fitting and scan conversion procedure (grayscale).
+        */
+       public static final OFTableName GASP = new OFTableName("gasp");
+
+       /**
+        * Glyph data.
+        */
+       public static final OFTableName GLYF = new OFTableName("glyf");
+
+       /**
+        * Horizontal device metrics.
+        */
+       public static final OFTableName HDMX = new OFTableName("hdmx");
+
+       /**
+        * Font header.
+        */
+       public static final OFTableName HEAD = new OFTableName("head");
+
+       /**
+        * Horizontal header.
+        */
+       public static final OFTableName HHEA = new OFTableName("hhea");
+
+       /**
+        * Horizontal metrics.
+        */
+       public static final OFTableName HMTX = new OFTableName("hmtx");
+
+       /**
+        * Kerning.
+        */
+       public static final OFTableName KERN = new OFTableName("kern");
+
+       /**
+        * Index to location.
+        */
+       public static final OFTableName LOCA = new OFTableName("loca");
+
+       /**
+        * Maximum profile.
+        */
+       public static final OFTableName MAXP = new OFTableName("maxp");
+
+       /**
+        * Naming table.
+        */
+       public static final OFTableName NAME = new OFTableName("name");
+
+       /**
+        * PostScript information.
+        */
+       public static final OFTableName POST = new OFTableName("post");
+
+       /**
+        * CVT Program.
+        */
+       public static final OFTableName PREP = new OFTableName("prep");
+
+       /**
+        * Vertical Metrics header.
+        */
+       public static final OFTableName VHEA = new OFTableName("vhea");
+
+       /**
+        * Vertical Metrics.
+        */
+       public static final OFTableName VMTX = new OFTableName("vmtx");
+
+       private final String name;
+
+       private OFTableName(String name) {
+               this.name = name;
+       }
+
+       /**
+        * Returns the name of the table as it should be in the Directory Table.
+        */
+       public String getName() {
+               return name;
+       }
+
+       /**
+        * Returns an instance of this class corresponding to the given string representation.
+        *
+        * @param tableName table name as in the Table Directory
+        * @return TTFTableName
+        */
+       public static OFTableName getValue(String tableName) {
+               if (tableName != null) {
+                       return new OFTableName(tableName);
+               }
+               throw new IllegalArgumentException("A TrueType font table name must not be null");
+       }
+
+       @Override
+       public int hashCode() {
+               return name.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object o) {
+               if (o == this) {
+                       return true;
+               }
+               if (!(o instanceof OFTableName)) {
+                       return false;
+               }
+               OFTableName to = (OFTableName) o;
+               return this.name.equals(to.getName());
+       }
+
+       @Override
+       public String toString() {
+               return name;
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OpenFont.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OpenFont.java
new file mode 100644 (file)
index 0000000..c37a012
--- /dev/null
@@ -0,0 +1,1734 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.truetype;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.AdvancedTypographicTableFormatException;
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphDefinitionTable;
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphPositioningTable;
+import com.jaredrummler.fontreader.fonts.*;
+
+import java.awt.*;
+import java.io.IOException;
+import java.util.List;
+import java.util.*;
+import java.util.Map.Entry;
+
+public abstract class OpenFont {
+
+       static final byte NTABS = 24;
+       static final int MAX_CHAR_CODE = 255;
+       static final int ENC_BUF_SIZE = 1024;
+
+       private static final String[] MAC_GLYPH_ORDERING = {
+                       ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent",
+                       "ampersand", "quotesingle", "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash",
+                       "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less",
+                       "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",
+                       "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum",
+                       "underscore", "grave", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r",
+                       "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "Adieresis", "Aring",
+                       "Ccedilla", "Eacute", "Ntilde", "Odieresis", "Udieresis", "aacute", "agrave", "acircumflex", "adieresis",
+                       "atilde", "aring", "ccedilla", "eacute", "egrave", "ecircumflex", "edieresis", "iacute", "igrave", "icircumflex",
+                       "idieresis", "ntilde", "oacute", "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave",
+                       "ucircumflex", "udieresis", "dagger", "degree", "cent", "sterling", "section", "bullet", "paragraph",
+                       "germandbls", "registered", "copyright", "trademark", "acute", "dieresis", "notequal", "AE", "Oslash", "infinity",
+                       "plusminus", "lessequal", "greaterequal", "yen", "mu", "partialdiff", "summation", "product", "pi", "integral",
+                       "ordfeminine", "ordmasculine", "Omega", "ae", "oslash", "questiondown", "exclamdown", "logicalnot", "radical",
+                       "florin", "approxequal", "Delta", "guillemotleft", "guillemotright", "ellipsis", "nonbreakingspace", "Agrave",
+                       "Atilde", "Otilde", "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft", "quoteright",
+                       "divide", "lozenge", "ydieresis", "Ydieresis", "fraction", "currency", "guilsinglleft", "guilsinglright", "fi",
+                       "fl", "daggerdbl", "periodcentered", "quotesinglbase", "quotedblbase", "perthousand", "Acircumflex",
+                       "Ecircumflex", "Aacute", "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Oacute",
+                       "Ocircumflex", "apple", "Ograve", "Uacute", "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde", "macron",
+                       "breve", "dotaccent", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "Lslash", "lslash", "Scaron",
+                       "scaron", "Zcaron", "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", "Thorn", "thorn", "minus",
+                       "multiply", "onesuperior", "twosuperior", "threesuperior", "onehalf", "onequarter", "threequarters", "franc",
+                       "Gbreve", "gbreve", "Idotaccent", "Scedilla", "scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat"
+       };
+
+       /**
+        * The FontFileReader used to read this TrueType font.
+        */
+       protected FontFileReader fontFile;
+
+       /**
+        * Set to true to get even more debug output than with level DEBUG
+        */
+       public static final boolean TRACE_ENABLED = false;
+
+       private static final String ENCODING = "WinAnsiEncoding";    // Default encoding
+
+       private static final short FIRST_CHAR = 0;
+
+       protected boolean useKerning;
+       private boolean isEmbeddable = true;
+       private boolean hasSerifs = true;
+       /**
+        * Table directory
+        */
+       protected Map<OFTableName, OFDirTabEntry> dirTabs;
+
+       private Map<Integer, Map<Integer, Integer>> rawKerningTab; // for CIDs
+       private Map<Integer, Map<Integer, Integer>> kerningTab; // for CIDs
+       private Map<Integer, Map<Integer, Integer>> ansiKerningTab; // For winAnsiEncoding
+       private List<CMapSegment> cmaps;
+       protected List<UnicodeMapping> unicodeMappings;
+
+       private int upem;                                // unitsPerEm from "head" table
+       protected int nhmtx;                               // Number of horizontal metrics
+       private PostScriptVersion postScriptVersion;
+       protected int locaFormat;
+       /**
+        * Offset to last loca
+        */
+       protected long lastLoca;
+       protected int numberOfGlyphs; // Number of glyphs in font (read from "maxp" table)
+
+       /**
+        * Contains glyph data
+        */
+       protected OFMtxEntry[] mtxTab;                  // Contains glyph data
+
+       protected String postScriptName = "";
+       protected String fullName = "";
+       protected String notice = "";
+       protected final Set<String> familyNames = new HashSet<>();
+       protected String subFamilyName = "";
+       protected boolean cid = true;
+
+       private long italicAngle;
+       private long isFixedPitch;
+       private int fontBBox1;
+       private int fontBBox2;
+       private int fontBBox3;
+       private int fontBBox4;
+       private int capHeight;
+       private int os2CapHeight;
+       private int underlinePosition;
+       private int underlineThickness;
+       private int strikeoutPosition;
+       private int strikeoutThickness;
+       private int xHeight;
+       private int os2xHeight;
+       //Effective ascender/descender
+       private int ascender;
+       private int descender;
+       //Ascender/descender from hhea table
+       private int hheaAscender;
+       private int hheaDescender;
+       //Ascender/descender from OS/2 table
+       private int os2Ascender;
+       private int os2Descender;
+       private int os2LineGap;
+       private int usWeightClass;
+
+       private short lastChar;
+
+       private int[] ansiWidth;
+       private Map<Integer, List<Integer>> ansiIndex;
+
+       // internal mapping of glyph indexes to unicode indexes
+       // used for quick mappings in this class
+       private final Map<Integer, Integer> glyphToUnicodeMap = new HashMap<>();
+       private final Map<Integer, Integer> unicodeToGlyphMap = new HashMap<>();
+
+       private boolean isCFF;
+
+       // advanced typographic table support
+       protected boolean useAdvanced;
+       protected OTFAdvancedTypographicTableReader advancedTableReader;
+
+       /**
+        * Version of the PostScript table (<q>post</q>) contained in this font.
+        */
+       public enum PostScriptVersion {
+               /**
+                * PostScript table version 1.0.
+                */
+               V1,
+               /**
+                * PostScript table version 2.0.
+                */
+               V2,
+               /**
+                * PostScript table version 3.0.
+                */
+               V3,
+               /**
+                * Unknown version of the PostScript table.
+                */
+               UNKNOWN
+       }
+
+       public OpenFont() {
+               this(true, false);
+       }
+
+       /**
+        * Constructor
+        *
+        * @param useKerning  true if kerning data should be loaded
+        * @param useAdvanced true if advanced typographic tables should be loaded
+        */
+       public OpenFont(boolean useKerning, boolean useAdvanced) {
+               this.useKerning = useKerning;
+               this.useAdvanced = useAdvanced;
+       }
+
+       /**
+        * Key-value helper class.
+        */
+       static final class UnicodeMapping implements Comparable {
+
+               private final int unicodeIndex;
+               private final int glyphIndex;
+
+               UnicodeMapping(OpenFont font, int glyphIndex, int unicodeIndex) {
+                       this.unicodeIndex = unicodeIndex;
+                       this.glyphIndex = glyphIndex;
+                       font.glyphToUnicodeMap.put(Integer.valueOf(glyphIndex), Integer.valueOf(unicodeIndex));
+                       font.unicodeToGlyphMap.put(Integer.valueOf(unicodeIndex), Integer.valueOf(glyphIndex));
+               }
+
+               /**
+                * Returns the glyphIndex.
+                *
+                * @return the glyph index
+                */
+               public int getGlyphIndex() {
+                       return glyphIndex;
+               }
+
+               /**
+                * Returns the unicodeIndex.
+                *
+                * @return the Unicode index
+                */
+               public int getUnicodeIndex() {
+                       return unicodeIndex;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int hashCode() {
+                       int hc = unicodeIndex;
+                       hc = 19 * hc + (hc ^ glyphIndex);
+                       return hc;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public boolean equals(Object o) {
+                       if (o instanceof UnicodeMapping) {
+                               UnicodeMapping m = (UnicodeMapping) o;
+                               if (unicodeIndex != m.unicodeIndex) {
+                                       return false;
+                               } else {
+                                       return (glyphIndex == m.glyphIndex);
+                               }
+                       } else {
+                               return false;
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               public int compareTo(Object o) {
+                       if (o instanceof UnicodeMapping) {
+                               UnicodeMapping m = (UnicodeMapping) o;
+                               if (unicodeIndex > m.unicodeIndex) {
+                                       return 1;
+                               } else if (unicodeIndex < m.unicodeIndex) {
+                                       return -1;
+                               } else {
+                                       return 0;
+                               }
+                       } else {
+                               return -1;
+                       }
+               }
+       }
+
+       /**
+        * Obtain directory table entry.
+        *
+        * @param name (tag) of entry
+        * @return a directory table entry or null if none found
+        */
+       public OFDirTabEntry getDirectoryEntry(OFTableName name) {
+               return dirTabs.get(name);
+       }
+
+       /**
+        * Position inputstream to position indicated
+        * in the dirtab offset + offset
+        *
+        * @param in        font file reader
+        * @param tableName (tag) of table
+        * @param offset    from start of table
+        * @return true if seek succeeded
+        * @throws IOException if I/O exception occurs during seek
+        */
+       public boolean seekTab(FontFileReader in, OFTableName tableName,
+                                                  long offset) throws IOException {
+               OFDirTabEntry dt = dirTabs.get(tableName);
+               if (dt == null) {
+                       return false;
+               } else {
+                       in.seekSet(dt.getOffset() + offset);
+               }
+               return true;
+       }
+
+       public int getUnitsPerEm() {
+               return upem;
+       }
+
+       public int getRawLineGap() {
+               return os2LineGap;
+       }
+
+       public int getLineGap() {
+               return convertTTFUnit2PDFUnit(os2LineGap);
+       }
+
+       /**
+        * Convert from truetype unit to pdf unit based on the
+        * unitsPerEm field in the "head" table
+        *
+        * @param n truetype unit
+        * @return pdf unit
+        */
+       public int convertTTFUnit2PDFUnit(int n) {
+               int ret;
+               if (n < 0) {
+                       long rest1 = n % upem;
+                       long storrest = 1000 * rest1;
+                       long ledd2 = (storrest != 0 ? rest1 / storrest : 0);
+                       ret = -((-1000 * n) / upem - (int) ledd2);
+               } else {
+                       ret = (n / upem) * 1000 + ((n % upem) * 1000) / upem;
+               }
+
+               return ret;
+       }
+
+       /**
+        * Read the cmap table,
+        * return false if the table is not present or only unsupported
+        * tables are present. Currently only unicode cmaps are supported.
+        * Set the unicodeIndex in the TTFMtxEntries and fills in the
+        * cmaps vector.
+        */
+       protected boolean readCMAP() throws IOException {
+
+               unicodeMappings = new ArrayList<>();
+
+               if (!seekTab(fontFile, OFTableName.CMAP, 2)) {
+                       return true;
+               }
+               int numCMap = fontFile.readTTFUShort();    // Number of cmap subtables
+               long cmapUniOffset = 0;
+               long symbolMapOffset = 0;
+
+               //Read offset for all tables. We are only interested in the unicode table
+               for (int i = 0; i < numCMap; i++) {
+                       int cmapPID = fontFile.readTTFUShort();
+                       int cmapEID = fontFile.readTTFUShort();
+                       long cmapOffset = fontFile.readTTFLong();
+
+                       if (cmapPID == 3 && cmapEID == 1) {
+                               cmapUniOffset = cmapOffset;
+                       }
+                       if (cmapPID == 3 && cmapEID == 0) {
+                               symbolMapOffset = cmapOffset;
+                       }
+               }
+
+               if (cmapUniOffset > 0) {
+                       return readUnicodeCmap(cmapUniOffset, 1);
+               } else if (symbolMapOffset > 0) {
+                       return readUnicodeCmap(symbolMapOffset, 0);
+               } else {
+                       return false;
+               }
+       }
+
+       private boolean readUnicodeCmap(long cmapUniOffset, int encodingID)
+                       throws IOException {
+               //Read CMAP table and correct mtxTab.index
+               int mtxPtr = 0;
+
+               // Read unicode cmap
+               seekTab(fontFile, OFTableName.CMAP, cmapUniOffset);
+               int cmapFormat = fontFile.readTTFUShort();
+               /*int cmap_length =*/
+               fontFile.readTTFUShort(); //skip cmap length
+
+               if (cmapFormat == 4) {
+                       fontFile.skip(2);    // Skip version number
+                       int cmapSegCountX2 = fontFile.readTTFUShort();
+                       int cmapSearchRange = fontFile.readTTFUShort();
+                       int cmapEntrySelector = fontFile.readTTFUShort();
+                       int cmapRangeShift = fontFile.readTTFUShort();
+
+                       int[] cmapEndCounts = new int[cmapSegCountX2 / 2];
+                       int[] cmapStartCounts = new int[cmapSegCountX2 / 2];
+                       int[] cmapDeltas = new int[cmapSegCountX2 / 2];
+                       int[] cmapRangeOffsets = new int[cmapSegCountX2 / 2];
+
+                       for (int i = 0; i < (cmapSegCountX2 / 2); i++) {
+                               cmapEndCounts[i] = fontFile.readTTFUShort();
+                       }
+
+                       fontFile.skip(2);    // Skip reservedPad
+
+                       for (int i = 0; i < (cmapSegCountX2 / 2); i++) {
+                               cmapStartCounts[i] = fontFile.readTTFUShort();
+                       }
+
+                       for (int i = 0; i < (cmapSegCountX2 / 2); i++) {
+                               cmapDeltas[i] = fontFile.readTTFShort();
+                       }
+
+                       //int startRangeOffset = in.getCurrentPos();
+
+                       for (int i = 0; i < (cmapSegCountX2 / 2); i++) {
+                               cmapRangeOffsets[i] = fontFile.readTTFUShort();
+                       }
+
+                       int glyphIdArrayOffset = fontFile.getCurrentPos();
+
+                       BitSet eightBitGlyphs = new BitSet(256);
+
+                       // Insert the unicode id for the glyphs in mtxTab
+                       // and fill in the cmaps ArrayList
+                       for (int i = 0; i < cmapStartCounts.length; i++) {
+
+                               for (int j = cmapStartCounts[i]; j <= cmapEndCounts[i]; j++) {
+
+                                       // Update lastChar
+                                       if (j < 256 && j > lastChar) {
+                                               lastChar = (short) j;
+                                       }
+
+                                       if (j < 256) {
+                                               eightBitGlyphs.set(j);
+                                       }
+
+                                       if (mtxPtr < mtxTab.length) {
+                                               int glyphIdx;
+                                               // the last character 65535 = .notdef
+                                               // may have a range offset
+                                               if (cmapRangeOffsets[i] != 0 && j != 65535) {
+                                                       int glyphOffset = glyphIdArrayOffset
+                                                                       + ((cmapRangeOffsets[i] / 2)
+                                                                       + (j - cmapStartCounts[i])
+                                                                       + (i)
+                                                                       - cmapSegCountX2 / 2) * 2;
+                                                       fontFile.seekSet(glyphOffset);
+                                                       glyphIdx = (fontFile.readTTFUShort() + cmapDeltas[i])
+                                                                       & 0xffff;
+                                                       //mtxTab[glyphIdx].setName(mtxTab[glyphIdx].getName() + " - "+(char)j);
+                                                       unicodeMappings.add(new UnicodeMapping(this, glyphIdx, j));
+                                                       mtxTab[glyphIdx].getUnicodeIndex().add(Integer.valueOf(j));
+
+                                                       if (encodingID == 0 && j >= 0xF020 && j <= 0xF0FF) {
+                                                               //Experimental: Mapping 0xF020-0xF0FF to 0x0020-0x00FF
+                                                               //Tested with Wingdings and Symbol TTF fonts which map their
+                                                               //glyphs in the region 0xF020-0xF0FF.
+                                                               int mapped = j - 0xF000;
+                                                               if (!eightBitGlyphs.get(mapped)) {
+                                                                       //Only map if Unicode code point hasn't been mapped before
+                                                                       unicodeMappings.add(new UnicodeMapping(this, glyphIdx, mapped));
+                                                                       mtxTab[glyphIdx].getUnicodeIndex().add(Integer.valueOf(mapped));
+                                                               }
+                                                       }
+
+                                                       // Also add winAnsiWidth
+                                                       List<Integer> v = ansiIndex.get(Integer.valueOf(j));
+                                                       if (v != null) {
+                                                               for (Integer aIdx : v) {
+                                                                       ansiWidth[aIdx.intValue()]
+                                                                                       = mtxTab[glyphIdx].getWx();
+
+                                                               }
+                                                       }
+
+                                               } else {
+                                                       glyphIdx = (j + cmapDeltas[i]) & 0xffff;
+
+                                                       if (glyphIdx < mtxTab.length) {
+                                                               mtxTab[glyphIdx].getUnicodeIndex().add(Integer.valueOf(j));
+                                                       }
+
+                                                       unicodeMappings.add(new UnicodeMapping(this, glyphIdx, j));
+                                                       if (glyphIdx < mtxTab.length) {
+                                                               mtxTab[glyphIdx].getUnicodeIndex().add(Integer.valueOf(j));
+                                                       }
+
+                                                       // Also add winAnsiWidth
+                                                       List<Integer> v = ansiIndex.get(Integer.valueOf(j));
+                                                       if (v != null) {
+                                                               for (Integer aIdx : v) {
+                                                                       ansiWidth[aIdx.intValue()] = mtxTab[glyphIdx].getWx();
+                                                               }
+                                                       }
+                                               }
+                                               if (glyphIdx < mtxTab.length) {
+                                                       if (mtxTab[glyphIdx].getUnicodeIndex().size() < 2) {
+                                                               mtxPtr++;
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               } else {
+                       return false;
+               }
+               return true;
+       }
+
+       private boolean isInPrivateUseArea(int start, int end) {
+               return (isInPrivateUseArea(start) || isInPrivateUseArea(end));
+       }
+
+       private boolean isInPrivateUseArea(int unicode) {
+               return (unicode >= 0xE000 && unicode <= 0xF8FF);
+       }
+
+       /**
+        * @return mmtx data
+        */
+       public List<OFMtxEntry> getMtx() {
+               return Collections.unmodifiableList(Arrays.asList(mtxTab));
+       }
+
+       /**
+        * Reads the font using a FontFileReader.
+        *
+        * @param in The FontFileReader to use
+        * @throws IOException In case of an I/O problem
+        */
+       public void readFont(FontFileReader in) throws IOException {
+               readFont(in, null);
+       }
+
+       /**
+        * Reads the font using a FontFileReader.
+        *
+        * @param in The FontFileReader to use
+        * @throws IOException In case of an I/O problem
+        */
+       public void readFont(FontFileReader in, String header) throws IOException {
+               readFont(in, header, null);
+       }
+
+       /**
+        * initialize the ansiWidths array (for winAnsiEncoding)
+        * and fill with the missingwidth
+        */
+       protected void initAnsiWidths() {
+               ansiWidth = new int[256];
+               for (int i = 0; i < 256; i++) {
+                       ansiWidth[i] = mtxTab[0].getWx();
+               }
+
+               // Create an index hash to the ansiWidth
+               // Can't just index the winAnsiEncoding when inserting widths
+               // same char (eg bullet) is repeated more than one place
+               ansiIndex = new HashMap<>();
+               for (int i = 32; i < Glyphs.WINANSI_ENCODING.length; i++) {
+                       Integer ansi = Integer.valueOf(i);
+                       Integer uni = Integer.valueOf(Glyphs.WINANSI_ENCODING[i]);
+
+                       List<Integer> v = ansiIndex.get(uni);
+                       if (v == null) {
+                               v = new ArrayList<>();
+                               ansiIndex.put(uni, v);
+                       }
+                       v.add(ansi);
+               }
+       }
+
+       /**
+        * Read the font data.
+        * If the fontfile is a TrueType Collection (.ttc file)
+        * the name of the font to read data for must be supplied,
+        * else the name is ignored.
+        *
+        * @param in   The FontFileReader to use
+        * @param name The name of the font
+        * @return boolean Returns true if the font is valid
+        * @throws IOException In case of an I/O problem
+        */
+       public boolean readFont(FontFileReader in, String header, String name) throws IOException {
+               initializeFont(in);
+               /*
+                * Check if TrueType collection, and that the name
+                * exists in the collection
+                */
+               if (!checkTTC(header, name)) {
+                       if (name == null) {
+                               throw new IllegalArgumentException("For TrueType collection you must specify which font to select (-ttcname)");
+                       } else {
+                               throw new IOException("Name does not exist in the TrueType collection: " + name);
+                       }
+               }
+
+               readDirTabs();
+               readFontHeader();
+               getNumGlyphs();
+
+               readHorizontalHeader();
+               readHorizontalMetrics();
+               initAnsiWidths();
+               readPostScript();
+               readOS2();
+               determineAscDesc();
+
+               readName();
+               boolean pcltFound = readPCLT();
+               // Read cmap table and fill in ansiwidths
+               boolean valid = readCMAP();
+               if (!valid) {
+                       return false;
+               }
+
+               // Create cmaps for bfentries
+               createCMaps();
+               updateBBoxAndOffset();
+
+               if (useKerning) {
+                       readKerning();
+               }
+               handleCharacterSpacing(in);
+
+               guessVerticalMetricsFromGlyphBBox();
+               return true;
+       }
+
+       protected abstract void updateBBoxAndOffset() throws IOException;
+
+       protected abstract void readName() throws IOException;
+
+       protected abstract void initializeFont(FontFileReader in) throws IOException;
+
+       protected void handleCharacterSpacing(FontFileReader in) throws IOException {
+               // Read advanced typographic tables.
+               if (useAdvanced) {
+                       try {
+                               OTFAdvancedTypographicTableReader atr
+                                               = new OTFAdvancedTypographicTableReader(this, in);
+                               atr.readAll();
+                               this.advancedTableReader = atr;
+                       } catch (AdvancedTypographicTableFormatException e) {
+                               e.printStackTrace();
+                       }
+               }
+
+       }
+
+       protected void createCMaps() {
+               cmaps = new ArrayList<>();
+               int unicodeStart;
+               int glyphStart;
+               int unicodeEnd;
+               if (unicodeMappings.isEmpty()) {
+                       return;
+               }
+               Iterator<UnicodeMapping> e = unicodeMappings.iterator();
+               UnicodeMapping um = e.next();
+               UnicodeMapping lastMapping = um;
+
+               unicodeStart = um.getUnicodeIndex();
+               glyphStart = um.getGlyphIndex();
+
+               while (e.hasNext()) {
+                       um = e.next();
+                       if (((lastMapping.getUnicodeIndex() + 1) != um.getUnicodeIndex())
+                                       || ((lastMapping.getGlyphIndex() + 1) != um.getGlyphIndex())) {
+                               unicodeEnd = lastMapping.getUnicodeIndex();
+                               cmaps.add(new CMapSegment(unicodeStart, unicodeEnd, glyphStart));
+                               unicodeStart = um.getUnicodeIndex();
+                               glyphStart = um.getGlyphIndex();
+                       }
+                       lastMapping = um;
+               }
+
+               unicodeEnd = lastMapping.getUnicodeIndex();
+               cmaps.add(new CMapSegment(unicodeStart, unicodeEnd, glyphStart));
+       }
+
+       /**
+        * Returns the PostScript name of the font.
+        *
+        * @return String The PostScript name
+        */
+       public String getPostScriptName() {
+               if (postScriptName.length() == 0) {
+                       return FontUtil.stripWhiteSpace(getFullName());
+               } else {
+                       return postScriptName;
+               }
+       }
+
+       PostScriptVersion getPostScriptVersion() {
+               return postScriptVersion;
+       }
+
+       /**
+        * Returns the font family names of the font.
+        *
+        * @return Set The family names (a Set of Strings)
+        */
+       public Set<String> getFamilyNames() {
+               return familyNames;
+       }
+
+       /**
+        * Returns the font sub family name of the font.
+        *
+        * @return String The sub family name
+        */
+       public String getSubFamilyName() {
+               return subFamilyName;
+       }
+
+       /**
+        * Returns the full name of the font.
+        *
+        * @return String The full name
+        */
+       public String getFullName() {
+               return fullName;
+       }
+
+       /**
+        * Returns the name of the character set used.
+        *
+        * @return String The caracter set
+        */
+       public String getCharSetName() {
+               return ENCODING;
+       }
+
+       /**
+        * Returns the CapHeight attribute of the font.
+        *
+        * @return int The CapHeight
+        */
+       public int getCapHeight() {
+               return convertTTFUnit2PDFUnit(capHeight);
+       }
+
+       /**
+        * Returns the XHeight attribute of the font.
+        *
+        * @return int The XHeight
+        */
+       public int getXHeight() {
+               return convertTTFUnit2PDFUnit(xHeight);
+       }
+
+       /**
+        * Returns the number of bytes necessary to pad the currentPosition so that a table begins
+        * on a 4-byte boundary.
+        *
+        * @param currentPosition the position to pad.
+        * @return int the number of bytes to pad.
+        */
+       protected int getPadSize(int currentPosition) {
+               int padSize = 4 - (currentPosition % 4);
+               return padSize < 4 ? padSize : 0;
+       }
+
+       /**
+        * Returns the Flags attribute of the font.
+        *
+        * @return int The Flags
+        */
+       public int getFlags() {
+               int flags = 32;    // Use Adobe Standard charset
+               if (italicAngle != 0) {
+                       flags |= 64;
+               }
+               if (isFixedPitch != 0) {
+                       flags |= 2;
+               }
+               if (hasSerifs) {
+                       flags |= 1;
+               }
+               return flags;
+       }
+
+       /**
+        * Returns the weight class of this font. Valid values are 100, 200....,800, 900.
+        *
+        * @return the weight class value (or 0 if there was no OS/2 table in the font)
+        */
+       public int getWeightClass() {
+               return this.usWeightClass;
+       }
+
+       /**
+        * Returns the StemV attribute of the font.
+        *
+        * @return String The StemV
+        */
+       public String getStemV() {
+               return "0";
+       }
+
+       /**
+        * Returns the ItalicAngle attribute of the font.
+        *
+        * @return String The ItalicAngle
+        */
+       public String getItalicAngle() {
+               return Short.toString((short) (italicAngle / 0x10000));
+       }
+
+       /**
+        * @return int[] The font bbox
+        */
+       public int[] getFontBBox() {
+               final int[] fbb = new int[4];
+               fbb[0] = convertTTFUnit2PDFUnit(fontBBox1);
+               fbb[1] = convertTTFUnit2PDFUnit(fontBBox2);
+               fbb[2] = convertTTFUnit2PDFUnit(fontBBox3);
+               fbb[3] = convertTTFUnit2PDFUnit(fontBBox4);
+
+               return fbb;
+       }
+
+       /**
+        * Returns the original bounding box values from the HEAD table
+        *
+        * @return An array of bounding box values
+        */
+       public int[] getBBoxRaw() {
+               return new int[]{fontBBox1, fontBBox2, fontBBox3, fontBBox4};
+       }
+
+       /**
+        * Returns the raw LowerCaseAscent attribute of the font.
+        *
+        * @return int The raw LowerCaseAscent
+        */
+       public int getRawLowerCaseAscent() {
+               return ascender;
+       }
+
+       /**
+        * Returns the raw LowerCaseDescent attribute of the font.
+        *
+        * @return int The raw LowerCaseDescent
+        */
+       public int getRawLowerCaseDescent() {
+               return descender;
+       }
+
+       /**
+        * Returns the LowerCaseAscent attribute of the font.
+        *
+        * @return int The LowerCaseAscent
+        */
+       public int getLowerCaseAscent() {
+               return convertTTFUnit2PDFUnit(ascender);
+       }
+
+       /**
+        * Returns the LowerCaseDescent attribute of the font.
+        *
+        * @return int The LowerCaseDescent
+        */
+       public int getLowerCaseDescent() {
+               return convertTTFUnit2PDFUnit(descender);
+       }
+
+       /**
+        * Returns the index of the last character, but this is for WinAnsiEncoding
+        * only, so the last char is < 256.
+        *
+        * @return short Index of the last character (<256)
+        */
+       public short getLastChar() {
+               return lastChar;
+       }
+
+       /**
+        * Returns the index of the first character.
+        *
+        * @return short Index of the first character
+        */
+       public short getFirstChar() {
+               return FIRST_CHAR;
+       }
+
+       /**
+        * Returns an array of character widths.
+        *
+        * @return int[] The character widths
+        */
+       public int[] getWidths() {
+               int[] wx = new int[mtxTab.length];
+               for (int i = 0; i < wx.length; i++) {
+                       wx[i] = convertTTFUnit2PDFUnit(mtxTab[i].getWx());
+               }
+               return wx;
+       }
+
+       public Rectangle[] getBoundingBoxes() {
+               Rectangle[] boundingBoxes = new Rectangle[mtxTab.length];
+               for (int i = 0; i < boundingBoxes.length; i++) {
+                       int[] boundingBox = mtxTab[i].getBoundingBox();
+                       boundingBoxes[i] = new Rectangle(
+                                       convertTTFUnit2PDFUnit(boundingBox[0]),
+                                       convertTTFUnit2PDFUnit(boundingBox[1]),
+                                       convertTTFUnit2PDFUnit(boundingBox[2] - boundingBox[0]),
+                                       convertTTFUnit2PDFUnit(boundingBox[3] - boundingBox[1]));
+               }
+               return boundingBoxes;
+       }
+
+       /**
+        * Returns an array (xMin, yMin, xMax, yMax) for a glyph.
+        *
+        * @param glyphIndex the index of the glyph
+        * @return int[] Array defining bounding box.
+        */
+       public int[] getBBox(int glyphIndex) {
+               int[] bboxInTTFUnits = mtxTab[glyphIndex].getBoundingBox();
+               int[] bbox = new int[4];
+               for (int i = 0; i < 4; i++) {
+                       bbox[i] = convertTTFUnit2PDFUnit(bboxInTTFUnits[i]);
+               }
+               return bbox;
+       }
+
+       /**
+        * Returns the width of a given character.
+        *
+        * @param idx Index of the character
+        * @return int Standard width
+        */
+       public int getCharWidth(int idx) {
+               return convertTTFUnit2PDFUnit(ansiWidth[idx]);
+       }
+
+       /**
+        * Returns the width of a given character in raw units
+        *
+        * @param idx Index of the character
+        * @return int Width in it's raw form stored in the font
+        */
+       public int getCharWidthRaw(int idx) {
+               if (ansiWidth != null) {
+                       return ansiWidth[idx];
+               }
+               return -1;
+       }
+
+       /**
+        * Returns the raw kerning table.
+        *
+        * @return Map The kerning table
+        */
+       public Map<Integer, Map<Integer, Integer>> getRawKerning() {
+               return rawKerningTab;
+       }
+
+       /**
+        * Returns the kerning table.
+        *
+        * @return Map The kerning table
+        */
+       public Map<Integer, Map<Integer, Integer>> getKerning() {
+               return kerningTab;
+       }
+
+       /**
+        * Returns the ANSI kerning table.
+        *
+        * @return Map The ANSI kerning table
+        */
+       public Map<Integer, Map<Integer, Integer>> getAnsiKerning() {
+               return ansiKerningTab;
+       }
+
+       public int getUnderlinePosition() {
+               return convertTTFUnit2PDFUnit(underlinePosition);
+       }
+
+       public int getUnderlineThickness() {
+               return convertTTFUnit2PDFUnit(underlineThickness);
+       }
+
+       public int getStrikeoutPosition() {
+               return convertTTFUnit2PDFUnit(strikeoutPosition);
+       }
+
+       public int getStrikeoutThickness() {
+               return convertTTFUnit2PDFUnit(strikeoutThickness);
+       }
+
+       /**
+        * Indicates if the font may be embedded.
+        *
+        * @return boolean True if it may be embedded
+        */
+       public boolean isEmbeddable() {
+               return isEmbeddable;
+       }
+
+       /**
+        * Indicates whether or not the font is an OpenType
+        * CFF font (rather than a TrueType font).
+        *
+        * @return true if the font is in OpenType CFF format.
+        */
+       public boolean isCFF() {
+               return this.isCFF;
+       }
+
+       /**
+        * Read Table Directory from the current position in the
+        * FontFileReader and fill the global HashMap dirTabs
+        * with the table name (String) as key and a TTFDirTabEntry
+        * as value.
+        *
+        * @throws IOException in case of an I/O problem
+        */
+       protected void readDirTabs() throws IOException {
+               int sfntVersion = fontFile.readTTFLong(); // TTF_FIXED_SIZE (4 bytes)
+               switch (sfntVersion) {
+                       case 0x10000:
+                               break;
+                       case 0x4F54544F: //"OTTO"
+                               this.isCFF = true;
+                               break;
+                       case 0x74727565: //"true"
+                               break;
+                       case 0x74797031: //"typ1"
+                               break;
+                       default:
+                               break;
+               }
+               int ntabs = fontFile.readTTFUShort();
+               fontFile.skip(6);    // 3xTTF_USHORT_SIZE
+
+               dirTabs = new HashMap<>();
+               OFDirTabEntry[] pd = new OFDirTabEntry[ntabs];
+
+               for (int i = 0; i < ntabs; i++) {
+                       pd[i] = new OFDirTabEntry();
+                       String tableName = pd[i].read(fontFile);
+                       dirTabs.put(OFTableName.getValue(tableName), pd[i]);
+               }
+               dirTabs.put(OFTableName.TABLE_DIRECTORY, new OFDirTabEntry(0L, fontFile.getCurrentPos()));
+       }
+
+       /**
+        * Read the "head" table, this reads the bounding box and
+        * sets the upem (unitsPerEM) variable
+        *
+        * @throws IOException in case of an I/O problem
+        */
+       protected void readFontHeader() throws IOException {
+               seekTab(fontFile, OFTableName.HEAD, 2 * 4 + 2 * 4);
+               int flags = fontFile.readTTFUShort();
+
+               upem = fontFile.readTTFUShort();
+
+               fontFile.skip(16);
+
+               fontBBox1 = fontFile.readTTFShort();
+               fontBBox2 = fontFile.readTTFShort();
+               fontBBox3 = fontFile.readTTFShort();
+               fontBBox4 = fontFile.readTTFShort();
+
+               fontFile.skip(2 + 2 + 2);
+
+               locaFormat = fontFile.readTTFShort();
+       }
+
+       /**
+        * Read the number of glyphs from the "maxp" table
+        *
+        * @throws IOException in case of an I/O problem
+        */
+       protected void getNumGlyphs() throws IOException {
+               seekTab(fontFile, OFTableName.MAXP, 4);
+               numberOfGlyphs = fontFile.readTTFUShort();
+       }
+
+       /**
+        * Read the "hhea" table to find the ascender and descender and
+        * size of "hmtx" table, as a fixed size font might have only
+        * one width.
+        *
+        * @throws IOException in case of an I/O problem
+        */
+       protected void readHorizontalHeader()
+                       throws IOException {
+               seekTab(fontFile, OFTableName.HHEA, 4);
+               hheaAscender = fontFile.readTTFShort();
+               hheaDescender = fontFile.readTTFShort();
+
+               fontFile.skip(2 + 2 + 3 * 2 + 8 * 2);
+               nhmtx = fontFile.readTTFUShort();
+       }
+
+       /**
+        * Read "hmtx" table and put the horizontal metrics
+        * in the mtxTab array. If the number of metrics is less
+        * than the number of glyphs (eg fixed size fonts), extend
+        * the mtxTab array and fill in the missing widths
+        *
+        * @throws IOException in case of an I/O problem
+        */
+       protected void readHorizontalMetrics()
+                       throws IOException {
+               seekTab(fontFile, OFTableName.HMTX, 0);
+
+               int mtxSize = Math.max(numberOfGlyphs, nhmtx);
+               mtxTab = new OFMtxEntry[mtxSize];
+
+               for (int i = 0; i < mtxSize; i++) {
+                       mtxTab[i] = new OFMtxEntry();
+               }
+               for (int i = 0; i < nhmtx; i++) {
+                       mtxTab[i].setWx(fontFile.readTTFUShort());
+                       mtxTab[i].setLsb(fontFile.readTTFUShort());
+               }
+
+               if (cid && nhmtx < mtxSize) {
+                       // Fill in the missing widths
+                       int lastWidth = mtxTab[nhmtx - 1].getWx();
+                       for (int i = nhmtx; i < mtxSize; i++) {
+                               mtxTab[i].setWx(lastWidth);
+                               mtxTab[i].setLsb(fontFile.readTTFUShort());
+                       }
+               }
+       }
+
+       /**
+        * Read the "post" table
+        * containing the PostScript names of the glyphs.
+        */
+       protected void readPostScript() throws IOException {
+               seekTab(fontFile, OFTableName.POST, 0);
+               int postFormat = fontFile.readTTFLong();
+               italicAngle = fontFile.readTTFULong();
+               underlinePosition = fontFile.readTTFShort();
+               underlineThickness = fontFile.readTTFShort();
+               isFixedPitch = fontFile.readTTFULong();
+
+               //Skip memory usage values
+               fontFile.skip(4 * 4);
+
+               switch (postFormat) {
+                       case 0x00010000:
+                               postScriptVersion = PostScriptVersion.V1;
+                               for (int i = 0; i < MAC_GLYPH_ORDERING.length; i++) {
+                                       mtxTab[i].setName(MAC_GLYPH_ORDERING[i]);
+                               }
+                               break;
+                       case 0x00020000:
+                               postScriptVersion = PostScriptVersion.V2;
+                               int numGlyphStrings = 257;
+
+                               // Read Number of Glyphs
+                               int l = fontFile.readTTFUShort();
+
+                               // Read indexes
+                               for (int i = 0; i < l; i++) {
+                                       mtxTab[i].setIndex(fontFile.readTTFUShort());
+
+                                       if (mtxTab[i].getIndex() > numGlyphStrings) {
+                                               numGlyphStrings = mtxTab[i].getIndex();
+                                       }
+
+                               }
+
+                               // firstChar=minIndex;
+                               String[] psGlyphsBuffer = new String[numGlyphStrings - 257];
+
+                               for (int i = 0; i < psGlyphsBuffer.length; i++) {
+                                       psGlyphsBuffer[i] = fontFile.readTTFString(fontFile.readTTFUByte());
+                               }
+
+                               //Set glyph names
+                               for (int i = 0; i < l; i++) {
+                                       if (mtxTab[i].getIndex() < MAC_GLYPH_ORDERING.length) {
+                                               mtxTab[i].setName(MAC_GLYPH_ORDERING[mtxTab[i].getIndex()]);
+                                       } else {
+                                               if (!mtxTab[i].isIndexReserved()) {
+                                                       int k = mtxTab[i].getIndex() - MAC_GLYPH_ORDERING.length;
+
+                                                       mtxTab[i].setName(psGlyphsBuffer[k]);
+                                               }
+                                       }
+                               }
+
+                               break;
+                       case 0x00030000:
+                               // PostScript format 3 contains no glyph names
+                               postScriptVersion = PostScriptVersion.V3;
+                               break;
+                       default:
+                               postScriptVersion = PostScriptVersion.UNKNOWN;
+               }
+       }
+
+       /**
+        * Read the "OS/2" table
+        */
+       protected void readOS2() throws IOException {
+               // Check if font is embeddable
+               OFDirTabEntry os2Entry = dirTabs.get(OFTableName.OS2);
+               if (os2Entry != null) {
+                       seekTab(fontFile, OFTableName.OS2, 0);
+                       int version = fontFile.readTTFUShort();
+
+                       fontFile.skip(2); //xAvgCharWidth
+                       this.usWeightClass = fontFile.readTTFUShort();
+
+                       // usWidthClass
+                       fontFile.skip(2);
+
+                       int fsType = fontFile.readTTFUShort();
+                       isEmbeddable = fsType != 2;
+                       fontFile.skip(8 * 2);
+                       strikeoutThickness = fontFile.readTTFShort();
+                       strikeoutPosition = fontFile.readTTFShort();
+                       fontFile.skip(2);
+                       fontFile.skip(10); //panose array
+                       fontFile.skip(4 * 4); //unicode ranges
+                       fontFile.skip(4);
+                       fontFile.skip(3 * 2);
+                       int v;
+                       os2Ascender = fontFile.readTTFShort(); //sTypoAscender
+                       os2Descender = fontFile.readTTFShort(); //sTypoDescender
+                       os2LineGap = fontFile.readTTFShort(); //sTypoLineGap
+
+                       v = fontFile.readTTFUShort(); //usWinAscent
+
+                       v = fontFile.readTTFUShort(); //usWinDescent
+
+                       //version 1 OS/2 table might end here
+                       if (os2Entry.getLength() >= 78 + (2 * 4) + (2 * 2)) {
+                               fontFile.skip(2 * 4);
+                               this.os2xHeight = fontFile.readTTFShort(); //sxHeight
+                               this.os2CapHeight = fontFile.readTTFShort(); //sCapHeight
+                       }
+
+               } else {
+                       isEmbeddable = true;
+               }
+       }
+
+       /**
+        * Read the "PCLT" table to find xHeight and capHeight.
+        *
+        * @throws IOException In case of a I/O problem
+        */
+       protected boolean readPCLT() throws IOException {
+               OFDirTabEntry dirTab = dirTabs.get(OFTableName.PCLT);
+               if (dirTab != null) {
+                       fontFile.seekSet(dirTab.getOffset() + 4 + 4 + 2);
+                       xHeight = fontFile.readTTFUShort();
+                       fontFile.skip(2 * 2);
+                       capHeight = fontFile.readTTFUShort();
+                       fontFile.skip(2 + 16 + 8 + 6 + 1 + 1);
+
+                       int serifStyle = fontFile.readTTFUByte();
+                       serifStyle = serifStyle >> 6;
+                       serifStyle = serifStyle & 3;
+                       hasSerifs = serifStyle != 1;
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Determines the right source for the ascender and descender values. The problem here is
+        * that the interpretation of these values is not the same for every font. There doesn't seem
+        * to be a uniform definition of an ascender and a descender. In some fonts
+        * the hhea values are defined after the Apple interpretation, but not in every font. The
+        * same problem is in the OS/2 table. FOP needs the ascender and descender to determine the
+        * baseline so we need values which add up more or less to the "em box". However, due to
+        * accent modifiers a character can grow beyond the em box.
+        */
+       protected void determineAscDesc() {
+               int hheaBoxHeight = hheaAscender - hheaDescender;
+               int os2BoxHeight = os2Ascender - os2Descender;
+               if (os2Ascender > 0 && os2BoxHeight <= upem) {
+                       ascender = os2Ascender;
+                       descender = os2Descender;
+               } else if (hheaAscender > 0 && hheaBoxHeight <= upem) {
+                       ascender = hheaAscender;
+                       descender = hheaDescender;
+               } else {
+                       if (os2Ascender > 0) {
+                               //Fall back to info from OS/2 if possible
+                               ascender = os2Ascender;
+                               descender = os2Descender;
+                       } else {
+                               ascender = hheaAscender;
+                               descender = hheaDescender;
+                       }
+               }
+       }
+
+       protected void guessVerticalMetricsFromGlyphBBox() {
+               // Approximate capHeight from height of "H"
+               // It's most unlikely that a font misses the PCLT table
+               // This also assumes that postscriptnames exists ("H")
+               // Should look it up in the cmap (that wouldn't help
+               // for charsets without H anyway...)
+               // Same for xHeight with the letter "x"
+               int localCapHeight = 0;
+               int localXHeight = 0;
+               int localAscender = 0;
+               int localDescender = 0;
+               for (int i = 0; i < mtxTab.length; i++) {
+                       if ("H".equals(mtxTab[i].getName())) {
+                               localCapHeight = mtxTab[i].getBoundingBox()[3];
+                       } else if ("x".equals(mtxTab[i].getName())) {
+                               localXHeight = mtxTab[i].getBoundingBox()[3];
+                       } else if ("d".equals(mtxTab[i].getName())) {
+                               localAscender = mtxTab[i].getBoundingBox()[3];
+                       } else if ("p".equals(mtxTab[i].getName())) {
+                               localDescender = mtxTab[i].getBoundingBox()[1];
+                       } else {
+                               // OpenType Fonts with a version 3.0 "post" table don't have glyph names.
+                               // Use Unicode indices instead.
+                               List unicodeIndex = mtxTab[i].getUnicodeIndex();
+                               if (unicodeIndex.size() > 0) {
+                                       //Only the first index is used
+                                       char ch = (char) ((Integer) unicodeIndex.get(0)).intValue();
+                                       if (ch == 'H') {
+                                               localCapHeight = mtxTab[i].getBoundingBox()[3];
+                                       } else if (ch == 'x') {
+                                               localXHeight = mtxTab[i].getBoundingBox()[3];
+                                       } else if (ch == 'd') {
+                                               localAscender = mtxTab[i].getBoundingBox()[3];
+                                       } else if (ch == 'p') {
+                                               localDescender = mtxTab[i].getBoundingBox()[1];
+                                       }
+                               }
+                       }
+               }
+               if (ascender - descender > upem) {
+                       ascender = localAscender;
+                       descender = localDescender;
+               }
+
+               if (capHeight == 0) {
+                       capHeight = localCapHeight;
+                       if (capHeight == 0) {
+                               capHeight = os2CapHeight;
+                       }
+               }
+               if (xHeight == 0) {
+                       xHeight = localXHeight;
+                       if (xHeight == 0) {
+                               xHeight = os2xHeight;
+                       }
+               }
+       }
+
+       /**
+        * Read the kerning table, create a table for both CIDs and
+        * winAnsiEncoding.
+        *
+        * @throws IOException In case of a I/O problem
+        */
+       protected void readKerning() throws IOException {
+               // Read kerning
+               rawKerningTab = new HashMap<>();
+               kerningTab = new HashMap<>();
+               ansiKerningTab = new HashMap<>();
+               OFDirTabEntry dirTab = dirTabs.get(OFTableName.KERN);
+               if (dirTab != null) {
+                       seekTab(fontFile, OFTableName.KERN, 2);
+                       for (int n = fontFile.readTTFUShort(); n > 0; n--) {
+                               fontFile.skip(2 * 2);
+                               int k = fontFile.readTTFUShort();
+                               if (!((k & 1) != 0) || (k & 2) != 0 || (k & 4) != 0) {
+                                       return;
+                               }
+                               if ((k >> 8) != 0) {
+                                       continue;
+                               }
+
+                               k = fontFile.readTTFUShort();
+                               fontFile.skip(3 * 2);
+                               while (k-- > 0) {
+                                       int i = fontFile.readTTFUShort();
+                                       int j = fontFile.readTTFUShort();
+                                       int kpx = fontFile.readTTFShort();
+                                       if (kpx != 0) {
+                                               Map<Integer, Integer> rawAdjTab = rawKerningTab.get(i);
+                                               if (rawAdjTab == null) {
+                                                       rawAdjTab = new HashMap<>();
+                                               }
+                                               rawAdjTab.put(j, kpx);
+                                               rawKerningTab.put(i, rawAdjTab);
+
+                                               // CID kerning table entry, using unicode indexes
+                                               final Integer iObj = glyphToUnicode(i);
+                                               final Integer u2 = glyphToUnicode(j);
+                                               if (iObj != null && u2 != null) {
+                                                       Map<Integer, Integer> adjTab = kerningTab.get(iObj);
+                                                       if (adjTab == null) {
+                                                               adjTab = new HashMap<>();
+                                                       }
+                                                       adjTab.put(u2, Integer.valueOf(convertTTFUnit2PDFUnit(kpx)));
+                                                       kerningTab.put(iObj, adjTab);
+                                               }
+                                       }
+                               }
+                       }
+
+                       // Create winAnsiEncoded kerning table from kerningTab
+                       // (could probably be simplified, for now we remap back to CID indexes and
+                       // then to winAnsi)
+
+                       for (Entry<Integer, Map<Integer, Integer>> e1 : kerningTab.entrySet()) {
+                               Integer unicodeKey1 = e1.getKey();
+                               Integer cidKey1 = unicodeToGlyph(unicodeKey1);
+                               Map<Integer, Integer> akpx = new HashMap<>();
+                               Map<Integer, Integer> ckpx = e1.getValue();
+
+                               for (Entry<Integer, Integer> e : ckpx.entrySet()) {
+                                       Integer unicodeKey2 = e.getKey();
+                                       Integer cidKey2 = unicodeToGlyph(unicodeKey2.intValue());
+                                       Integer kern = e.getValue();
+
+                                       Iterator uniMap = mtxTab[cidKey2.intValue()].getUnicodeIndex().listIterator();
+                                       while (uniMap.hasNext()) {
+                                               Integer unicodeKey = (Integer) uniMap.next();
+                                               Integer[] ansiKeys = unicodeToWinAnsi(unicodeKey.intValue());
+                                               for (int u = 0; u < ansiKeys.length; u++) {
+                                                       akpx.put(ansiKeys[u], kern);
+                                               }
+                                       }
+                               }
+
+                               if (akpx.size() > 0) {
+                                       Iterator uniMap = mtxTab[cidKey1.intValue()].getUnicodeIndex().listIterator();
+                                       while (uniMap.hasNext()) {
+                                               Integer unicodeKey = (Integer) uniMap.next();
+                                               Integer[] ansiKeys = unicodeToWinAnsi(unicodeKey.intValue());
+                                               for (Integer ansiKey : ansiKeys) {
+                                                       ansiKerningTab.put(ansiKey, akpx);
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Streams a font.
+        *
+        * @param ttfOut The interface for streaming TrueType tables.
+        * @throws IOException file write error
+        */
+       public void stream(TTFOutputStream ttfOut) throws IOException {
+               SortedSet<Entry<OFTableName, OFDirTabEntry>> sortedDirTabs = sortDirTabMap(dirTabs);
+               byte[] file = fontFile.getAllBytes();
+               TTFTableOutputStream tableOut = ttfOut.getTableOutputStream();
+               TTFGlyphOutputStream glyphOut = ttfOut.getGlyphOutputStream();
+               ttfOut.startFontStream();
+               for (Entry<OFTableName, OFDirTabEntry> entry : sortedDirTabs) {
+                       int offset = (int) entry.getValue().getOffset();
+                       int paddedLength = (int) entry.getValue().getLength();
+                       paddedLength += getPadSize(offset + paddedLength);
+                       if (entry.getKey().equals(OFTableName.GLYF)) {
+                               streamGlyf(glyphOut, file, offset, paddedLength);
+                       } else {
+                               tableOut.streamTable(file, offset, paddedLength);
+                       }
+               }
+               ttfOut.endFontStream();
+       }
+
+       private void streamGlyf(TTFGlyphOutputStream glyphOut, byte[] fontFile, int tableOffset,
+                                                       int tableLength) throws IOException {
+               //Stream all but the last glyph
+               int glyphStart = 0;
+               int glyphEnd = 0;
+               glyphOut.startGlyphStream();
+               for (int i = 0; i < mtxTab.length - 1; i++) {
+                       glyphStart = (int) mtxTab[i].getOffset() + tableOffset;
+                       glyphEnd = (int) mtxTab[i + 1].getOffset() + tableOffset;
+                       glyphOut.streamGlyph(fontFile, glyphStart, glyphEnd - glyphStart);
+               }
+               glyphOut.streamGlyph(fontFile, glyphEnd, (tableOffset + tableLength) - glyphEnd);
+               glyphOut.endGlyphStream();
+       }
+
+       /**
+        * Returns the order in which the tables in a TrueType font should be written to file.
+        *
+        * @param directoryTabs the map that is to be sorted.
+        * @return TTFTablesNames[] an array of table names sorted in the order they should appear in
+        * the TTF file.
+        */
+       SortedSet<Entry<OFTableName, OFDirTabEntry>>
+       sortDirTabMap(Map<OFTableName, OFDirTabEntry> directoryTabs) {
+               SortedSet<Entry<OFTableName, OFDirTabEntry>> sortedSet
+                               = new TreeSet<>(
+                               new Comparator<>() {
+
+                                       public int compare(Entry<OFTableName, OFDirTabEntry> o1,
+                                                                          Entry<OFTableName, OFDirTabEntry> o2) {
+                                               return (int) (o1.getValue().getOffset() - o2.getValue().getOffset());
+                                       }
+                               });
+               // @SuppressFBWarnings("DMI_ENTRY_SETS_MAY_REUSE_ENTRY_OBJECTS")
+               sortedSet.addAll(directoryTabs.entrySet());
+               return sortedSet;
+       }
+
+       /**
+        * Returns this font's character to glyph mapping.
+        *
+        * @return the font's cmap
+        */
+       public List<CMapSegment> getCMaps() {
+               return cmaps;
+       }
+
+       /**
+        * Check if this is a TrueType collection and that the given
+        * name exists in the collection.
+        * If it does, set offset in fontfile to the beginning of
+        * the Table Directory for that font.
+        *
+        * @param name The name to check
+        * @return True if not collection or font name present, false otherwise
+        * @throws IOException In case of an I/O problem
+        */
+       protected final boolean checkTTC(String tag, String name) throws IOException {
+               if ("ttcf".equals(tag)) {
+                       // This is a TrueType Collection
+                       fontFile.skip(4);
+
+                       // Read directory offsets
+                       int numDirectories = (int) fontFile.readTTFULong();
+                       // int numDirectories=in.readTTFUShort();
+                       long[] dirOffsets = new long[numDirectories];
+                       for (int i = 0; i < numDirectories; i++) {
+                               dirOffsets[i] = fontFile.readTTFULong();
+                       }
+
+                       // Read all the directories and name tables to check
+                       // If the font exists - this is a bit ugly, but...
+                       boolean found = false;
+
+                       // Iterate through all name tables even if font
+                       // Is found, just to show all the names
+                       long dirTabOffset = 0;
+                       for (int i = 0; (i < numDirectories); i++) {
+                               fontFile.seekSet(dirOffsets[i]);
+                               readDirTabs();
+
+                               readName();
+
+                               if (fullName.equals(name)) {
+                                       found = true;
+                                       dirTabOffset = dirOffsets[i];
+                               }
+
+                               // Reset names
+                               notice = "";
+                               fullName = "";
+                               familyNames.clear();
+                               postScriptName = "";
+                               subFamilyName = "";
+                       }
+
+                       fontFile.seekSet(dirTabOffset);
+                       return found;
+               } else {
+                       fontFile.seekSet(0);
+                       return true;
+               }
+       }
+
+       /**
+        * Return TTC font names
+        *
+        * @param in FontFileReader to read from
+        * @return True if not collection or font name present, false otherwise
+        * @throws IOException In case of an I/O problem
+        */
+       public final List<String> getTTCnames(FontFileReader in) throws IOException {
+               this.fontFile = in;
+
+               List<String> fontNames = new ArrayList<>();
+               String tag = in.readTTFString(4);
+
+               if ("ttcf".equals(tag)) {
+                       // This is a TrueType Collection
+                       in.skip(4);
+
+                       // Read directory offsets
+                       int numDirectories = (int) in.readTTFULong();
+                       long[] dirOffsets = new long[numDirectories];
+                       for (int i = 0; i < numDirectories; i++) {
+                               dirOffsets[i] = in.readTTFULong();
+                       }
+
+                       for (int i = 0; (i < numDirectories); i++) {
+                               in.seekSet(dirOffsets[i]);
+                               readDirTabs();
+
+                               readName();
+
+                               fontNames.add(fullName);
+
+                               // Reset names
+                               notice = "";
+                               fullName = "";
+                               familyNames.clear();
+                               postScriptName = "";
+                               subFamilyName = "";
+                       }
+
+                       in.seekSet(0);
+                       return fontNames;
+               } else {
+                       return null;
+               }
+       }
+
+       /*
+        * Helper classes, they are not very efficient, but that really
+        * doesn't matter...
+        */
+       private Integer[] unicodeToWinAnsi(int unicode) {
+               List<Integer> ret = new ArrayList<>();
+               for (int i = 32; i < Glyphs.WINANSI_ENCODING.length; i++) {
+                       if (unicode == Glyphs.WINANSI_ENCODING[i]) {
+                               ret.add(Integer.valueOf(i));
+                       }
+               }
+               return ret.toArray(new Integer[ret.size()]);
+       }
+
+       private String formatUnitsForDebug(int units) {
+               return units + " -> " + convertTTFUnit2PDFUnit(units) + " internal units";
+       }
+
+       /**
+        * Map a glyph index to the corresponding unicode code point
+        *
+        * @param glyphIndex
+        * @return unicode code point
+        */
+       public Integer glyphToUnicode(int glyphIndex) {
+               return glyphToUnicodeMap.get(Integer.valueOf(glyphIndex));
+       }
+
+       /**
+        * Map a unicode code point to the corresponding glyph index
+        *
+        * @param unicodeIndex unicode code point
+        * @return glyph index
+        */
+       public Integer unicodeToGlyph(int unicodeIndex) throws IOException {
+               final Integer result
+                               = unicodeToGlyphMap.get(Integer.valueOf(unicodeIndex));
+               if (result == null) {
+                       throw new IOException(
+                                       "Glyph index not found for unicode value " + unicodeIndex);
+               }
+               return result;
+       }
+
+       String getGlyphName(int glyphIndex) {
+               return mtxTab[glyphIndex].getName();
+       }
+
+       /**
+        * Determine if advanced (typographic) table is present.
+        *
+        * @return true if advanced (typographic) table is present
+        */
+       public boolean hasAdvancedTable() {
+               if (advancedTableReader != null) {
+                       return advancedTableReader.hasAdvancedTable();
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Returns the GDEF table or null if none present.
+        *
+        * @return the GDEF table
+        */
+       public GlyphDefinitionTable getGDEF() {
+               if (advancedTableReader != null) {
+                       return advancedTableReader.getGDEF();
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Returns the GSUB table or null if none present.
+        *
+        * @return the GSUB table
+        */
+       public GlyphSubstitutionTable getGSUB() {
+               if (advancedTableReader != null) {
+                       return advancedTableReader.getGSUB();
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Returns the GPOS table or null if none present.
+        *
+        * @return the GPOS table
+        */
+       public GlyphPositioningTable getGPOS() {
+               if (advancedTableReader != null) {
+                       return advancedTableReader.getGPOS();
+               } else {
+                       return null;
+               }
+       }
+
+       public String getCopyrightNotice() {
+               return notice;
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFFile.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFFile.java
new file mode 100644 (file)
index 0000000..06c0a9d
--- /dev/null
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.truetype;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Reads a TrueType file or a TrueType Collection.
+ * The TrueType spec can be found at the Microsoft.
+ * Typography site: http://www.microsoft.com/truetype/
+ */
+public class TTFFile extends OpenFont {
+
+       /**
+        * Reads a TTF file
+        *
+        * @param file The font file
+        * @return The TrueType file
+        * @throws IOException if an IO error occurs
+        */
+       public static TTFFile open(File file) throws IOException {
+               return open(new FileInputStream(file));
+       }
+
+       /**
+        * Reads a TTF file from an InputStream
+        *
+        * @param is InputStream to read from
+        * @return The TrueType file
+        * @throws IOException if an IO error occurs
+        */
+       public static TTFFile open(InputStream is) throws IOException {
+               TTFFile ttfFile = new TTFFile();
+               ttfFile.readFont(new FontFileReader(is));
+               return ttfFile;
+       }
+
+       public TTFFile() {
+               this(true, false);
+       }
+
+       /**
+        * Constructor
+        *
+        * @param useKerning  true if kerning data should be loaded
+        * @param useAdvanced true if advanced typographic tables should be loaded
+        */
+       public TTFFile(boolean useKerning, boolean useAdvanced) {
+               super(useKerning, useAdvanced);
+       }
+
+       /**
+        * Read the "name" table.
+        *
+        * @throws IOException In case of a I/O problem
+        */
+       protected void readName() throws IOException {
+               seekTab(fontFile, OFTableName.NAME, 2);
+               int i = fontFile.getCurrentPos();
+               int n = fontFile.readTTFUShort();
+               int j = fontFile.readTTFUShort() + i - 2;
+               i += 2 * 2;
+
+               while (n-- > 0) {
+                       fontFile.seekSet(i);
+                       final int platformID = fontFile.readTTFUShort();
+                       final int encodingID = fontFile.readTTFUShort();
+                       final int languageID = fontFile.readTTFUShort();
+
+                       int k = fontFile.readTTFUShort();
+                       int l = fontFile.readTTFUShort();
+
+                       if (((platformID == 1 || platformID == 3)
+                                       && (encodingID == 0 || encodingID == 1))) {
+                               fontFile.seekSet(j + fontFile.readTTFUShort());
+                               String txt;
+                               if (platformID == 3) {
+                                       txt = fontFile.readTTFString(l, encodingID);
+                               } else {
+                                       txt = fontFile.readTTFString(l);
+                               }
+
+                               switch (k) {
+                                       case 0:
+                                               if (notice.length() == 0) {
+                                                       notice = txt;
+                                               }
+                                               break;
+                                       case 1: //Font Family Name
+                                       case 16: //Preferred Family
+                                               familyNames.add(txt);
+                                               break;
+                                       case 2:
+                                               if (subFamilyName.length() == 0) {
+                                                       subFamilyName = txt;
+                                               }
+                                               break;
+                                       case 4:
+                                               if (fullName.length() == 0 || (platformID == 3 && languageID == 1033)) {
+                                                       fullName = txt;
+                                               }
+                                               break;
+                                       case 6:
+                                               if (postScriptName.length() == 0) {
+                                                       postScriptName = txt;
+                                               }
+                                               break;
+                                       default:
+                                               break;
+                               }
+                       }
+                       i += 6 * 2;
+               }
+       }
+
+       /**
+        * Read the "glyf" table to find the bounding boxes.
+        *
+        * @throws IOException In case of a I/O problem
+        */
+       private void readGlyf() throws IOException {
+               OFDirTabEntry dirTab = dirTabs.get(OFTableName.GLYF);
+               if (dirTab == null) {
+                       throw new IOException("glyf table not found, cannot continue");
+               }
+               for (int i = 0; i < (numberOfGlyphs - 1); i++) {
+                       if (mtxTab[i].getOffset() != mtxTab[i + 1].getOffset()) {
+                               fontFile.seekSet(dirTab.getOffset() + mtxTab[i].getOffset());
+                               fontFile.skip(2);
+                               final int[] bbox = {
+                                               fontFile.readTTFShort(),
+                                               fontFile.readTTFShort(),
+                                               fontFile.readTTFShort(),
+                                               fontFile.readTTFShort()};
+                               mtxTab[i].setBoundingBox(bbox);
+                       } else {
+                               mtxTab[i].setBoundingBox(mtxTab[0].getBoundingBox());
+                       }
+               }
+
+               long n = (dirTabs.get(OFTableName.GLYF)).getOffset();
+               for (int i = 0; i < numberOfGlyphs; i++) {
+                       if ((i + 1) >= mtxTab.length
+                                       || mtxTab[i].getOffset() != mtxTab[i + 1].getOffset()) {
+                               fontFile.seekSet(n + mtxTab[i].getOffset());
+                               fontFile.skip(2);
+                               final int[] bbox = {
+                                               fontFile.readTTFShort(),
+                                               fontFile.readTTFShort(),
+                                               fontFile.readTTFShort(),
+                                               fontFile.readTTFShort()};
+                               mtxTab[i].setBoundingBox(bbox);
+                       } else {
+                               final int bbox0 = mtxTab[0].getBoundingBox()[0];
+                               final int[] bbox = {bbox0, bbox0, bbox0, bbox0};
+                               mtxTab[i].setBoundingBox(bbox);
+                       }
+               }
+       }
+
+       @Override
+       protected void updateBBoxAndOffset() throws IOException {
+               readIndexToLocation();
+               readGlyf();
+       }
+
+       /**
+        * Read the "loca" table.
+        *
+        * @throws IOException In case of a I/O problem
+        */
+       protected final void readIndexToLocation()
+                       throws IOException {
+               if (!seekTab(fontFile, OFTableName.LOCA, 0)) {
+                       throw new IOException("'loca' table not found, happens when the font file doesn't"
+                                       + " contain TrueType outlines (trying to read an OpenType CFF font maybe?)");
+               }
+               for (int i = 0; i < numberOfGlyphs; i++) {
+                       mtxTab[i].setOffset(locaFormat == 1 ? fontFile.readTTFULong()
+                                       : (fontFile.readTTFUShort() << 1));
+               }
+               lastLoca = (locaFormat == 1 ? fontFile.readTTFULong()
+                               : (fontFile.readTTFUShort() << 1));
+       }
+
+       /**
+        * Gets the last location of the glyf table
+        *
+        * @return The last location as a long
+        */
+       public long getLastGlyfLocation() {
+               return lastLoca;
+       }
+
+       @Override
+       protected void initializeFont(FontFileReader in) throws IOException {
+               fontFile = in;
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFGlyphOutputStream.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFGlyphOutputStream.java
new file mode 100644 (file)
index 0000000..0b38935
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.truetype;
+
+import java.io.IOException;
+
+/**
+ * An interface for writing individual glyphs from the glyf table of a TrueType font to an output stream.
+ */
+public interface TTFGlyphOutputStream {
+
+       /**
+        * Begins the streaming of glyphs.
+        */
+       void startGlyphStream() throws IOException;
+
+       /**
+        * Streams an individual glyph from the given byte array.
+        *
+        * @param glyphData the source of the glyph data to stream from
+        * @param offset    the position in the glyph data where the glyph starts
+        * @param size      the size of the glyph data in bytes
+        */
+       void streamGlyph(byte[] glyphData, int offset, int size) throws IOException;
+
+       /**
+        * Ends the streaming of glyphs.
+        */
+       void endGlyphStream() throws IOException;
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFOutputStream.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFOutputStream.java
new file mode 100644 (file)
index 0000000..8b3d334
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.truetype;
+
+import java.io.IOException;
+
+/**
+ * An interface for writing a TrueType font to an output stream.
+ */
+public interface TTFOutputStream {
+
+       /**
+        * Starts writing the font.
+        */
+       void startFontStream() throws IOException;
+
+       /**
+        * Returns an object for streaming TrueType tables.
+        */
+       TTFTableOutputStream getTableOutputStream();
+
+       /**
+        * Returns an object for streaming TrueType glyphs in the glyf table.
+        */
+       TTFGlyphOutputStream getGlyphOutputStream();
+
+       /**
+        * Ends writing the font.
+        */
+       void endFontStream() throws IOException;
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFTableOutputStream.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFTableOutputStream.java
new file mode 100644 (file)
index 0000000..edfa9a6
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.truetype;
+
+import java.io.IOException;
+
+/**
+ * An interface for writing a TrueType table to an output stream.
+ */
+public interface TTFTableOutputStream {
+
+       /**
+        * Streams a table from the given byte array.
+        *
+        * @param ttfData the source of the table to stream from
+        * @param offset  the position in the byte array where the table starts
+        * @param size    the size of the table in bytes
+        */
+       void streamTable(byte[] ttfData, int offset, int size) throws IOException;
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/util/CharUtilities.java b/fontparser/src/main/java/com/jaredrummler/fontreader/util/CharUtilities.java
new file mode 100644 (file)
index 0000000..f5b9fe7
--- /dev/null
@@ -0,0 +1,417 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.util;
+
+/**
+ * This class provides utilities to distinguish various kinds of Unicode
+ * whitespace and to get character widths in a given FontState.
+ */
+public class CharUtilities {
+
+       /**
+        * Character code used to signal a character boundary in
+        * inline content, such as an inline with borders and padding
+        * or a nested block object.
+        */
+       public static final char CODE_EOT = 0;
+
+       /**
+        * Character class: Unicode white space
+        */
+       public static final int UCWHITESPACE = 0;
+       /**
+        * Character class: Line feed
+        */
+       public static final int LINEFEED = 1;
+       /**
+        * Character class: Boundary between text runs
+        */
+       public static final int EOT = 2;
+       /**
+        * Character class: non-whitespace
+        */
+       public static final int NONWHITESPACE = 3;
+       /**
+        * Character class: XML whitespace
+        */
+       public static final int XMLWHITESPACE = 4;
+
+       /**
+        * null char
+        */
+       public static final char NULL_CHAR = '\u0000';
+       /**
+        * linefeed character
+        */
+       public static final char LINEFEED_CHAR = '\n';
+       /**
+        * carriage return
+        */
+       public static final char CARRIAGE_RETURN = '\r';
+       /**
+        * normal tab
+        */
+       public static final char TAB = '\t';
+       /**
+        * normal space
+        */
+       public static final char SPACE = '\u0020';
+       /**
+        * non-breaking space
+        */
+       public static final char NBSPACE = '\u00A0';
+       /**
+        * next line control character
+        */
+       public static final char NEXT_LINE = '\u0085';
+       /**
+        * zero-width space
+        */
+       public static final char ZERO_WIDTH_SPACE = '\u200B';
+       /**
+        * word joiner
+        */
+       public static final char WORD_JOINER = '\u2060';
+       /**
+        * zero-width joiner
+        */
+       public static final char ZERO_WIDTH_JOINER = '\u200D';
+       /**
+        * left-to-right mark
+        */
+       public static final char LRM = '\u200E';
+       /**
+        * right-to-left mark
+        */
+       public static final char RLM = '\u202F';
+       /**
+        * left-to-right embedding
+        */
+       public static final char LRE = '\u202A';
+       /**
+        * right-to-left embedding
+        */
+       public static final char RLE = '\u202B';
+       /**
+        * pop directional formatting
+        */
+       public static final char PDF = '\u202C';
+       /**
+        * left-to-right override
+        */
+       public static final char LRO = '\u202D';
+       /**
+        * right-to-left override
+        */
+       public static final char RLO = '\u202E';
+       /**
+        * zero-width no-break space (= byte order mark)
+        */
+       public static final char ZERO_WIDTH_NOBREAK_SPACE = '\uFEFF';
+       /**
+        * soft hyphen
+        */
+       public static final char SOFT_HYPHEN = '\u00AD';
+       /**
+        * line-separator
+        */
+       public static final char LINE_SEPARATOR = '\u2028';
+       /**
+        * paragraph-separator
+        */
+       public static final char PARAGRAPH_SEPARATOR = '\u2029';
+       /**
+        * missing ideograph
+        */
+       public static final char MISSING_IDEOGRAPH = '\u25A1';
+       /**
+        * Ideogreaphic space
+        */
+       public static final char IDEOGRAPHIC_SPACE = '\u3000';
+       /**
+        * Object replacement character
+        */
+       public static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
+       /**
+        * Unicode value indicating the the character is "not a character".
+        */
+       public static final char NOT_A_CHARACTER = '\uFFFF';
+
+       /**
+        * Utility class: Constructor prevents instantiating when subclassed.
+        */
+       protected CharUtilities() {
+               throw new UnsupportedOperationException();
+       }
+
+       /**
+        * Return the appropriate CharClass constant for the type
+        * of the passed character.
+        *
+        * @param c character to inspect
+        * @return the determined character class
+        */
+       public static int classOf(int c) {
+               switch (c) {
+                       case CODE_EOT:
+                               return EOT;
+                       case LINEFEED_CHAR:
+                               return LINEFEED;
+                       case SPACE:
+                       case CARRIAGE_RETURN:
+                       case TAB:
+                               return XMLWHITESPACE;
+                       default:
+                               return isAnySpace(c) ? UCWHITESPACE : NONWHITESPACE;
+               }
+       }
+
+       /**
+        * Helper method to determine if the character is a
+        * space with normal behavior. Normal behavior means that
+        * it's not non-breaking.
+        *
+        * @param c character to inspect
+        * @return True if the character is a normal space
+        */
+       public static boolean isBreakableSpace(int c) {
+               return (c == SPACE || isFixedWidthSpace(c));
+       }
+
+       /**
+        * Method to determine if the character is a zero-width space.
+        *
+        * @param c the character to check
+        * @return true if the character is a zero-width space
+        */
+       public static boolean isZeroWidthSpace(int c) {
+               return c == ZERO_WIDTH_SPACE           // 200Bh
+                               || c == WORD_JOINER                // 2060h
+                               || c == ZERO_WIDTH_NOBREAK_SPACE;  // FEFFh (also used as BOM)
+       }
+
+       /**
+        * Method to determine if the character is a (breakable) fixed-width space.
+        *
+        * @param c the character to check
+        * @return true if the character has a fixed-width
+        */
+       public static boolean isFixedWidthSpace(int c) {
+               return (c >= '\u2000' && c <= '\u200B')
+                               || c == '\u3000';
+//      c == '\u2000'                   // en quad
+//      c == '\u2001'                   // em quad
+//      c == '\u2002'                   // en space
+//      c == '\u2003'                   // em space
+//      c == '\u2004'                   // three-per-em space
+//      c == '\u2005'                   // four-per-em space
+//      c == '\u2006'                   // six-per-em space
+//      c == '\u2007'                   // figure space
+//      c == '\u2008'                   // punctuation space
+//      c == '\u2009'                   // thin space
+//      c == '\u200A'                   // hair space
+//      c == '\u200B'                   // zero width space
+//      c == '\u3000'                   // ideographic space
+       }
+
+       /**
+        * Method to determine if the character is a nonbreaking
+        * space.
+        *
+        * @param c character to check
+        * @return True if the character is a nbsp
+        */
+       public static boolean isNonBreakableSpace(int c) {
+               return
+                               (c == NBSPACE       // no-break space
+                                               || c == '\u202F'    // narrow no-break space
+                                               || c == '\u3000'    // ideographic space
+                                               || c == WORD_JOINER // word joiner
+                                               || c == ZERO_WIDTH_NOBREAK_SPACE);  // zero width no-break space
+       }
+
+       /**
+        * Method to determine if the character is an adjustable
+        * space.
+        *
+        * @param c character to check
+        * @return True if the character is adjustable
+        */
+       public static boolean isAdjustableSpace(int c) {
+               //TODO: are there other kinds of adjustable spaces?
+               return
+                               (c == '\u0020'    // normal space
+                                               || c == NBSPACE); // no-break space
+       }
+
+       /**
+        * Determines if the character represents any kind of space.
+        *
+        * @param c character to check
+        * @return True if the character represents any kind of space
+        */
+       public static boolean isAnySpace(int c) {
+               return (isBreakableSpace(c) || isNonBreakableSpace(c));
+       }
+
+       /**
+        * Indicates whether a character is classified as "Alphabetic" by the Unicode standard.
+        *
+        * @param c the character
+        * @return true if the character is "Alphabetic"
+        */
+       public static boolean isAlphabetic(int c) {
+               //http://www.unicode.org/Public/UNIDATA/UCD.html#Alphabetic
+               //Generated from: Other_Alphabetic + Lu + Ll + Lt + Lm + Lo + Nl
+               int generalCategory = Character.getType((char) c);
+               switch (generalCategory) {
+                       case Character.UPPERCASE_LETTER: //Lu
+                       case Character.LOWERCASE_LETTER: //Ll
+                       case Character.TITLECASE_LETTER: //Lt
+                       case Character.MODIFIER_LETTER: //Lm
+                       case Character.OTHER_LETTER: //Lo
+                       case Character.LETTER_NUMBER: //Nl
+                               return true;
+                       default:
+                               //TODO if (ch in Other_Alphabetic) return true; (Probably need ICU4J for that)
+                               //Other_Alphabetic contains mostly more exotic characters
+                               return false;
+               }
+       }
+
+       /**
+        * Indicates whether the given character is an explicit break-character
+        *
+        * @param c the character to check
+        * @return true if the character represents an explicit break
+        */
+       public static boolean isExplicitBreak(int c) {
+               return (c == LINEFEED_CHAR
+                               || c == CARRIAGE_RETURN
+                               || c == NEXT_LINE
+                               || c == LINE_SEPARATOR
+                               || c == PARAGRAPH_SEPARATOR);
+       }
+
+       /**
+        * Convert a single unicode scalar value to an XML numeric character
+        * reference. If in the BMP, four digits are used, otherwise 6 digits are used.
+        *
+        * @param c a unicode scalar value
+        * @return a string representing a numeric character reference
+        */
+       public static String charToNCRef(int c) {
+               StringBuffer sb = new StringBuffer();
+               for (int i = 0, nDigits = (c > 0xFFFF) ? 6 : 4; i < nDigits; i++, c >>= 4) {
+                       int d = c & 0xF;
+                       char hd;
+                       if (d < 10) {
+                               hd = (char) ((int) '0' + d);
+                       } else {
+                               hd = (char) ((int) 'A' + (d - 10));
+                       }
+                       sb.append(hd);
+               }
+               return "&#x" + sb.reverse() + ";";
+       }
+
+       /**
+        * Convert a string to a sequence of ASCII or XML numeric character references.
+        *
+        * @param s a java string (encoded in UTF-16)
+        * @return a string representing a sequence of numeric character reference or
+        * ASCII characters
+        */
+       public static String toNCRefs(String s) {
+               StringBuffer sb = new StringBuffer();
+               if (s != null) {
+                       for (int i = 0; i < s.length(); i++) {
+                               char c = s.charAt(i);
+                               if ((c >= 32) && (c < 127)) {
+                                       if (c == '<') {
+                                               sb.append("&lt;");
+                                       } else if (c == '>') {
+                                               sb.append("&gt;");
+                                       } else if (c == '&') {
+                                               sb.append("&amp;");
+                                       } else {
+                                               sb.append(c);
+                                       }
+                               } else {
+                                       sb.append(charToNCRef(c));
+                               }
+                       }
+               }
+               return sb.toString();
+       }
+
+       /**
+        * Pad a string S on left out to width W using padding character PAD.
+        *
+        * @param s     string to pad
+        * @param width width of field to add padding
+        * @param pad   character to use for padding
+        * @return padded string
+        */
+       public static String padLeft(String s, int width, char pad) {
+               StringBuffer sb = new StringBuffer();
+               for (int i = s.length(); i < width; i++) {
+                       sb.append(pad);
+               }
+               sb.append(s);
+               return sb.toString();
+       }
+
+       /**
+        * Format character for debugging output, which it is prefixed with "0x", padded left with '0'
+        * and either 4 or 6 hex characters in width according to whether it is in the BMP or not.
+        *
+        * @param c character code
+        * @return formatted character string
+        */
+       public static String format(int c) {
+               if (c < 1114112) {
+                       return "0x" + padLeft(Integer.toString(c, 16), (c < 65536) ? 4 : 6, '0');
+               } else {
+                       return "!NOT A CHARACTER!";
+               }
+       }
+
+       /**
+        * Determine if two character sequences contain the same characters.
+        *
+        * @param cs1 first character sequence
+        * @param cs2 second character sequence
+        * @return true if both sequences have same length and same character sequence
+        */
+       public static boolean isSameSequence(CharSequence cs1, CharSequence cs2) {
+               assert cs1 != null;
+               assert cs2 != null;
+               if (cs1.length() != cs2.length()) {
+                       return false;
+               } else {
+                       for (int i = 0, n = cs1.length(); i < n; i++) {
+                               if (cs1.charAt(i) != cs2.charAt(i)) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphSequence.java b/fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphSequence.java
new file mode 100644 (file)
index 0000000..e85cc09
--- /dev/null
@@ -0,0 +1,655 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.util;
+
+import com.jaredrummler.fontreader.complexscripts.util.CharAssociation;
+
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+// CSOFF: LineLengthCheck
+
+/**
+ * <p>A GlyphSequence encapsulates a sequence of character codes, a sequence of glyph codes,
+ * and a sequence of character associations, where, for each glyph in the sequence of glyph
+ * codes, there is a corresponding character association. Character associations server to
+ * relate the glyph codes in a glyph sequence to the specific characters in an original
+ * character code sequence with which the glyph codes are associated.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public class GlyphSequence implements Cloneable {
+
+       /**
+        * default character buffer capacity in case new character buffer is created
+        */
+       private static final int DEFAULT_CHARS_CAPACITY = 8;
+
+       /**
+        * character buffer
+        */
+       private IntBuffer characters;
+       /**
+        * glyph buffer
+        */
+       private IntBuffer glyphs;
+       /**
+        * association list
+        */
+       private List associations;
+       /**
+        * predications flag
+        */
+       private boolean predications;
+
+       /**
+        * Instantiate a glyph sequence, reusing (i.e., not copying) the referenced
+        * character and glyph buffers and associations. If characters is null, then
+        * an empty character buffer is created. If glyphs is null, then a glyph buffer
+        * is created whose capacity is that of the character buffer. If associations is
+        * null, then identity associations are created.
+        *
+        * @param characters   a (possibly null) buffer of associated (originating) characters
+        * @param glyphs       a (possibly null) buffer of glyphs
+        * @param associations a (possibly null) array of glyph to character associations
+        * @param predications true if predications are enabled
+        */
+       public GlyphSequence(IntBuffer characters, IntBuffer glyphs, List associations, boolean predications) {
+               if (characters == null) {
+                       characters = IntBuffer.allocate(DEFAULT_CHARS_CAPACITY);
+               }
+               if (glyphs == null) {
+                       glyphs = IntBuffer.allocate(characters.capacity());
+               }
+               if (associations == null) {
+                       associations = makeIdentityAssociations(characters.limit(), glyphs.limit());
+               }
+               this.characters = characters;
+               this.glyphs = glyphs;
+               this.associations = associations;
+               this.predications = predications;
+       }
+
+       /**
+        * Instantiate a glyph sequence, reusing (i.e., not copying) the referenced
+        * character and glyph buffers and associations. If characters is null, then
+        * an empty character buffer is created. If glyphs is null, then a glyph buffer
+        * is created whose capacity is that of the character buffer. If associations is
+        * null, then identity associations are created.
+        *
+        * @param characters   a (possibly null) buffer of associated (originating) characters
+        * @param glyphs       a (possibly null) buffer of glyphs
+        * @param associations a (possibly null) array of glyph to character associations
+        */
+       public GlyphSequence(IntBuffer characters, IntBuffer glyphs, List associations) {
+               this(characters, glyphs, associations, false);
+       }
+
+       /**
+        * Instantiate a glyph sequence using an existing glyph sequence, where the new glyph sequence shares
+        * the character array of the existing sequence (but not the buffer object), and creates new copies
+        * of glyphs buffer and association list.
+        *
+        * @param gs an existing glyph sequence
+        */
+       public GlyphSequence(GlyphSequence gs) {
+               this(gs.characters.duplicate(), copyBuffer(gs.glyphs), copyAssociations(gs.associations), gs.predications);
+       }
+
+       /**
+        * Instantiate a glyph sequence using an existing glyph sequence, where the new glyph sequence shares
+        * the character array of the existing sequence (but not the buffer object), but uses the specified
+        * backtrack, input, and lookahead glyph arrays to populate the glyphs, and uses the specified
+        * of glyphs buffer and association list.
+        * backtrack, input, and lookahead association arrays to populate the associations.
+        *
+        * @param gs  an existing glyph sequence
+        * @param bga backtrack glyph array
+        * @param iga input glyph array
+        * @param lga lookahead glyph array
+        * @param bal backtrack association list
+        * @param ial input association list
+        * @param lal lookahead association list
+        */
+       public GlyphSequence(GlyphSequence gs, int[] bga, int[] iga, int[] lga, CharAssociation[] bal, CharAssociation[] ial,
+                                                CharAssociation[] lal) {
+               this(gs.characters.duplicate(), concatGlyphs(bga, iga, lga), concatAssociations(bal, ial, lal), gs.predications);
+       }
+
+       /**
+        * Obtain reference to underlying character buffer.
+        *
+        * @return character buffer reference
+        */
+       public IntBuffer getCharacters() {
+               return characters;
+       }
+
+       /**
+        * Obtain array of characters. If <code>copy</code> is true, then
+        * a newly instantiated array is returned, otherwise a reference to
+        * the underlying buffer's array is returned. N.B. in case a reference
+        * to the undelying buffer's array is returned, the length
+        * of the array is not necessarily the number of characters in array.
+        * To determine the number of characters, use {@link #getCharacterCount}.
+        *
+        * @param copy true if to return a newly instantiated array of characters
+        * @return array of characters
+        */
+       public int[] getCharacterArray(boolean copy) {
+               if (copy) {
+                       return toArray(characters);
+               } else {
+                       return characters.array();
+               }
+       }
+
+       /**
+        * Obtain the number of characters in character array, where
+        * each character constitutes a unicode scalar value.
+        *
+        * @return number of characters available in character array
+        */
+       public int getCharacterCount() {
+               return characters.limit();
+       }
+
+       /**
+        * Obtain glyph id at specified index.
+        *
+        * @param index to obtain glyph
+        * @return the glyph identifier of glyph at specified index
+        * @throws IndexOutOfBoundsException if index is less than zero
+        *                                   or exceeds last valid position
+        */
+       public int getGlyph(int index) throws IndexOutOfBoundsException {
+               return glyphs.get(index);
+       }
+
+       /**
+        * Set glyph id at specified index.
+        *
+        * @param index to set glyph
+        * @param gi    glyph index
+        * @throws IndexOutOfBoundsException if index is greater or equal to
+        *                                   the limit of the underlying glyph buffer
+        */
+       public void setGlyph(int index, int gi) throws IndexOutOfBoundsException {
+               if (gi > 65535) {
+                       gi = 65535;
+               }
+               glyphs.put(index, gi);
+       }
+
+       /**
+        * Obtain reference to underlying glyph buffer.
+        *
+        * @return glyph buffer reference
+        */
+       public IntBuffer getGlyphs() {
+               return glyphs;
+       }
+
+       /**
+        * Obtain count glyphs starting at offset. If <code>count</code> is
+        * negative, then it is treated as if the number of available glyphs
+        * were specified.
+        *
+        * @param offset into glyph sequence
+        * @param count  of glyphs to obtain starting at offset, or negative,
+        *               indicating all avaialble glyphs starting at offset
+        * @return glyph array
+        */
+       public int[] getGlyphs(int offset, int count) {
+               int ng = getGlyphCount();
+               if (offset < 0) {
+                       offset = 0;
+               } else if (offset > ng) {
+                       offset = ng;
+               }
+               if (count < 0) {
+                       count = ng - offset;
+               }
+               int[] ga = new int[count];
+               for (int i = offset, n = offset + count, k = 0; i < n; i++) {
+                       if (k < ga.length) {
+                               ga[k++] = glyphs.get(i);
+                       }
+               }
+               return ga;
+       }
+
+       /**
+        * Obtain array of glyphs. If <code>copy</code> is true, then
+        * a newly instantiated array is returned, otherwise a reference to
+        * the underlying buffer's array is returned. N.B. in case a reference
+        * to the undelying buffer's array is returned, the length
+        * of the array is not necessarily the number of glyphs in array.
+        * To determine the number of glyphs, use {@link #getGlyphCount}.
+        *
+        * @param copy true if to return a newly instantiated array of glyphs
+        * @return array of glyphs
+        */
+       public int[] getGlyphArray(boolean copy) {
+               if (copy) {
+                       return toArray(glyphs);
+               } else {
+                       return glyphs.array();
+               }
+       }
+
+       /**
+        * Obtain the number of glyphs in glyphs array, where
+        * each glyph constitutes a font specific glyph index.
+        *
+        * @return number of glyphs available in character array
+        */
+       public int getGlyphCount() {
+               return glyphs.limit();
+       }
+
+       /**
+        * Obtain association at specified index.
+        *
+        * @param index into associations array
+        * @return glyph to character associations at specified index
+        * @throws IndexOutOfBoundsException if index is less than zero
+        *                                   or exceeds last valid position
+        */
+       public CharAssociation getAssociation(int index) throws IndexOutOfBoundsException {
+               return (CharAssociation) associations.get(index);
+       }
+
+       /**
+        * Obtain reference to underlying associations list.
+        *
+        * @return associations list
+        */
+       public List getAssociations() {
+               return associations;
+       }
+
+       /**
+        * Obtain count associations starting at offset.
+        *
+        * @param offset into glyph sequence
+        * @param count  of associations to obtain starting at offset, or negative,
+        *               indicating all avaialble associations starting at offset
+        * @return associations
+        */
+       public CharAssociation[] getAssociations(int offset, int count) {
+               int ng = getGlyphCount();
+               if (offset < 0) {
+                       offset = 0;
+               } else if (offset > ng) {
+                       offset = ng;
+               }
+               if (count < 0) {
+                       count = ng - offset;
+               }
+               CharAssociation[] aa = new CharAssociation[count];
+               for (int i = offset, n = offset + count, k = 0; i < n; i++) {
+                       if (k < aa.length) {
+                               aa[k++] = (CharAssociation) associations.get(i);
+                       }
+               }
+               return aa;
+       }
+
+       /**
+        * Enable or disable predications.
+        *
+        * @param enable true if predications are to be enabled; otherwise false to disable
+        */
+       public void setPredications(boolean enable) {
+               this.predications = enable;
+       }
+
+       /**
+        * Obtain predications state.
+        *
+        * @return true if predications are enabled
+        */
+       public boolean getPredications() {
+               return this.predications;
+       }
+
+       /**
+        * Set predication <KEY,VALUE> at glyph sequence OFFSET.
+        *
+        * @param offset offset (index) into glyph sequence
+        * @param key    predication key
+        * @param value  predication value
+        */
+       public void setPredication(int offset, String key, Object value) {
+               if (predications) {
+                       CharAssociation[] aa = getAssociations(offset, 1);
+                       CharAssociation ca = aa[0];
+                       ca.setPredication(key, value);
+               }
+       }
+
+       /**
+        * Get predication KEY at glyph sequence OFFSET.
+        *
+        * @param offset offset (index) into glyph sequence
+        * @param key    predication key
+        * @return predication KEY at OFFSET or null if none exists
+        */
+       public Object getPredication(int offset, String key) {
+               if (predications) {
+                       CharAssociation[] aa = getAssociations(offset, 1);
+                       CharAssociation ca = aa[0];
+                       return ca.getPredication(key);
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Compare glyphs.
+        *
+        * @param gb buffer containing glyph indices with which this glyph sequence's glyphs are to be compared
+        * @return zero if glyphs are the same, otherwise returns 1 or -1 according to whether this glyph sequence's
+        * glyphs are lexicographically greater or lesser than the glyphs in the specified string buffer
+        */
+       public int compareGlyphs(IntBuffer gb) {
+               int ng = getGlyphCount();
+               for (int i = 0, n = gb.limit(); i < n; i++) {
+                       if (i < ng) {
+                               int g1 = glyphs.get(i);
+                               int g2 = gb.get(i);
+                               if (g1 > g2) {
+                                       return 1;
+                               } else if (g1 < g2) {
+                                       return -1;
+                               }
+                       } else {
+                               return -1;              // this gb is a proper prefix of specified gb
+                       }
+               }
+               return 0;                       // same lengths with no difference
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public Object clone() {
+               try {
+                       GlyphSequence gs = (GlyphSequence) super.clone();
+                       gs.characters = copyBuffer(characters);
+                       gs.glyphs = copyBuffer(glyphs);
+                       gs.associations = copyAssociations(associations);
+                       return gs;
+               } catch (CloneNotSupportedException e) {
+                       return null;
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public String toString() {
+               StringBuffer sb = new StringBuffer();
+               sb.append('{');
+               sb.append("chars = [");
+               sb.append(characters);
+               sb.append("], glyphs = [");
+               sb.append(glyphs);
+               sb.append("], associations = [");
+               sb.append(associations);
+               sb.append("]");
+               sb.append('}');
+               return sb.toString();
+       }
+
+       /**
+        * Determine if two arrays of glyphs are identical.
+        *
+        * @param ga1 first glyph array
+        * @param ga2 second glyph array
+        * @return true if arrays are botth null or both non-null and have identical elements
+        */
+       public static boolean sameGlyphs(int[] ga1, int[] ga2) {
+               if (ga1 == ga2) {
+                       return true;
+               } else if ((ga1 == null) || (ga2 == null)) {
+                       return false;
+               } else if (ga1.length != ga2.length) {
+                       return false;
+               } else {
+                       for (int i = 0, n = ga1.length; i < n; i++) {
+                               if (ga1[i] != ga2[i]) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+       }
+
+       /**
+        * Concatenante glyph arrays.
+        *
+        * @param bga backtrack glyph array
+        * @param iga input glyph array
+        * @param lga lookahead glyph array
+        * @return new integer buffer containing concatenated glyphs
+        */
+       public static IntBuffer concatGlyphs(int[] bga, int[] iga, int[] lga) {
+               int ng = 0;
+               if (bga != null) {
+                       ng += bga.length;
+               }
+               if (iga != null) {
+                       ng += iga.length;
+               }
+               if (lga != null) {
+                       ng += lga.length;
+               }
+               IntBuffer gb = IntBuffer.allocate(ng);
+               if (bga != null) {
+                       gb.put(bga);
+               }
+               if (iga != null) {
+                       gb.put(iga);
+               }
+               if (lga != null) {
+                       gb.put(lga);
+               }
+               gb.flip();
+               return gb;
+       }
+
+       /**
+        * Concatenante association arrays.
+        *
+        * @param baa backtrack association array
+        * @param iaa input association array
+        * @param laa lookahead association array
+        * @return new list containing concatenated associations
+        */
+       public static List concatAssociations(CharAssociation[] baa, CharAssociation[] iaa, CharAssociation[] laa) {
+               int na = 0;
+               if (baa != null) {
+                       na += baa.length;
+               }
+               if (iaa != null) {
+                       na += iaa.length;
+               }
+               if (laa != null) {
+                       na += laa.length;
+               }
+               if (na > 0) {
+                       List gl = new ArrayList(na);
+                       if (baa != null) {
+                               for (int i = 0; i < baa.length; i++) {
+                                       gl.add(baa[i]);
+                               }
+                       }
+                       if (iaa != null) {
+                               for (int i = 0; i < iaa.length; i++) {
+                                       gl.add(iaa[i]);
+                               }
+                       }
+                       if (laa != null) {
+                               for (int i = 0; i < laa.length; i++) {
+                                       gl.add(laa[i]);
+                               }
+                       }
+                       return gl;
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Join (concatenate) glyph sequences.
+        *
+        * @param gs original glyph sequence from which to reuse character array reference
+        * @param sa array of glyph sequences, whose glyph arrays and association lists are to be concatenated
+        * @return new glyph sequence referring to character array of GS and concatenated glyphs and associations of SA
+        */
+       public static GlyphSequence join(GlyphSequence gs, GlyphSequence[] sa) {
+               assert sa != null;
+               int tg = 0;
+               int ta = 0;
+               for (int i = 0, n = sa.length; i < n; i++) {
+                       GlyphSequence s = sa[i];
+                       IntBuffer ga = s.getGlyphs();
+                       assert ga != null;
+                       int ng = ga.limit();
+                       List al = s.getAssociations();
+                       assert al != null;
+                       int na = al.size();
+                       assert na == ng;
+                       tg += ng;
+                       ta += na;
+               }
+               IntBuffer uga = IntBuffer.allocate(tg);
+               ArrayList ual = new ArrayList(ta);
+               for (int i = 0, n = sa.length; i < n; i++) {
+                       GlyphSequence s = sa[i];
+                       uga.put(s.getGlyphs());
+                       ual.addAll(s.getAssociations());
+               }
+               return new GlyphSequence(gs.getCharacters(), uga, ual, gs.getPredications());
+       }
+
+       /**
+        * Reorder sequence such that [SOURCE,SOURCE+COUNT) is moved just prior to TARGET.
+        *
+        * @param gs     input sequence
+        * @param source index of sub-sequence to reorder
+        * @param count  length of sub-sequence to reorder
+        * @param target index to which source sub-sequence is to be moved
+        * @return reordered sequence (or original if no reordering performed)
+        */
+       public static GlyphSequence reorder(GlyphSequence gs, int source, int count, int target) {
+               if (source != target) {
+                       int ng = gs.getGlyphCount();
+                       int[] ga = gs.getGlyphArray(false);
+                       int[] nga = new int[ng];
+                       CharAssociation[] aa = gs.getAssociations(0, ng);
+                       CharAssociation[] naa = new CharAssociation[ng];
+                       if (source < target) {
+                               int t = 0;
+                               for (int s = 0, e = source; s < e; s++, t++) {
+                                       nga[t] = ga[s];
+                                       naa[t] = aa[s];
+                               }
+                               for (int s = source + count, e = target; s < e; s++, t++) {
+                                       nga[t] = ga[s];
+                                       naa[t] = aa[s];
+                               }
+                               for (int s = source, e = source + count; s < e; s++, t++) {
+                                       nga[t] = ga[s];
+                                       naa[t] = aa[s];
+                               }
+                               for (int s = target, e = ng; s < e; s++, t++) {
+                                       nga[t] = ga[s];
+                                       naa[t] = aa[s];
+                               }
+                       } else {
+                               int t = 0;
+                               for (int s = 0, e = target; s < e; s++, t++) {
+                                       nga[t] = ga[s];
+                                       naa[t] = aa[s];
+                               }
+                               for (int s = source, e = source + count; s < e; s++, t++) {
+                                       nga[t] = ga[s];
+                                       naa[t] = aa[s];
+                               }
+                               for (int s = target, e = source; s < e; s++, t++) {
+                                       nga[t] = ga[s];
+                                       naa[t] = aa[s];
+                               }
+                               for (int s = source + count, e = ng; s < e; s++, t++) {
+                                       nga[t] = ga[s];
+                                       naa[t] = aa[s];
+                               }
+                       }
+                       return new GlyphSequence(gs, null, nga, null, null, naa, null);
+               } else {
+                       return gs;
+               }
+       }
+
+       private static int[] toArray(IntBuffer ib) {
+               if (ib != null) {
+                       int n = ib.limit();
+                       int[] ia = new int[n];
+                       ib.get(ia, 0, n);
+                       return ia;
+               } else {
+                       return new int[0];
+               }
+       }
+
+       private static List makeIdentityAssociations(int numChars, int numGlyphs) {
+               int nc = numChars;
+               int ng = numGlyphs;
+               List av = new ArrayList(ng);
+               for (int i = 0, n = ng; i < n; i++) {
+                       int k = (i > nc) ? nc : i;
+                       av.add(new CharAssociation(i, (k == nc) ? 0 : 1));
+               }
+               return av;
+       }
+
+       private static IntBuffer copyBuffer(IntBuffer ib) {
+               if (ib != null) {
+                       int[] ia = new int[ib.capacity()];
+                       int p = ib.position();
+                       int l = ib.limit();
+                       System.arraycopy(ib.array(), 0, ia, 0, ia.length);
+                       return IntBuffer.wrap(ia, p, l - p);
+               } else {
+                       return null;
+               }
+       }
+
+       private static List copyAssociations(List ca) {
+               if (ca != null) {
+                       return new ArrayList(ca);
+               } else {
+                       return ca;
+               }
+       }
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphTester.java b/fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphTester.java
new file mode 100644 (file)
index 0000000..efb9e2e
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.util;
+
+/**
+ * <p>Interface for testing glyph properties according to glyph identifier.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface GlyphTester {
+
+       /**
+        * Perform a test on a glyph identifier.
+        *
+        * @param gi    glyph identififer
+        * @param flags that apply to lookup in scope
+        * @return true if test is satisfied
+        */
+       boolean test(int gi, int flags);
+
+}
diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/util/ScriptContextTester.java b/fontparser/src/main/java/com/jaredrummler/fontreader/util/ScriptContextTester.java
new file mode 100644 (file)
index 0000000..a7da671
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 Jared Rummler <jared.rummler@gmail.com>
+ *
+ * Licensed 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.
+ *
+ */
+
+package com.jaredrummler.fontreader.util;
+
+import com.jaredrummler.fontreader.complexscripts.fonts.GlyphContextTester;
+
+/**
+ * <p>Interface for providing script specific context testers.</p>
+ *
+ * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
+ */
+public interface ScriptContextTester {
+
+       /**
+        * Obtain a glyph context tester for the specified feature.
+        *
+        * @param feature a feature identifier
+        * @return a glyph context tester or null if none available for the specified feature
+        */
+       GlyphContextTester getTester(String feature);
+
+}
index b7bc9a15e0e52048ba0fe54f01c078da795fb8ba..a67fe345a5ea4e0fb58eb673c95131bb6d428a60 100644 (file)
@@ -2,4 +2,5 @@
 rootProject.name = "factbooks"
 
 include("externals")
+include("fontparser")
 //include("fightgame")
index 701ccb1da9f85167000c94332706e3fd27f74d90..46f72bc075480027a2c2a3f6853633270f57ed2a 100644 (file)
@@ -1,21 +1,25 @@
 package info.mechyrdia.lore
 
+import com.jaredrummler.fontreader.truetype.FontFileReader
+import com.jaredrummler.fontreader.truetype.TTFFile
+import com.jaredrummler.fontreader.util.GlyphSequence
 import info.mechyrdia.Configuration
 import info.mechyrdia.yieldThread
 import io.ktor.util.*
 import java.awt.Font
-import java.awt.RenderingHints
 import java.awt.Shape
 import java.awt.geom.AffineTransform
 import java.awt.geom.GeneralPath
 import java.awt.geom.PathIterator
 import java.awt.image.BufferedImage
+import java.io.ByteArrayInputStream
 import java.io.File
+import java.io.IOException
+import java.nio.IntBuffer
 import java.util.concurrent.locks.ReentrantLock
 import kotlin.concurrent.withLock
 import kotlin.properties.ReadOnlyProperty
 import kotlin.reflect.KProperty
-import kotlin.text.toCharArray
 
 object MechyrdiaSansFont {
        enum class Alignment(val amount: Double) {
@@ -23,39 +27,49 @@ object MechyrdiaSansFont {
        }
        
        fun renderTextToSvg(text: String, bold: Boolean, italic: Boolean, align: Alignment): String {
-               return layoutText(text, getFont(bold, italic), align.amount).toSvgDocument()
+               val (file, font) = getFont(bold, italic)
+               return layoutText(text, file, font, align.amount).toSvgDocument(96.0 / file.unitsPerEm)
        }
        
-       private const val DEFAULT_FONT_SIZE = 48f
-       
        private val fontsRoot = File(Configuration.CurrentConfiguration.rootDir, "fonts")
        
-       private fun loadedFont(fontName: String): ReadOnlyProperty<Any?, Font> {
-               return object : ReadOnlyProperty<Any?, Font> {
+       private fun loadedFont(fontName: String): ReadOnlyProperty<Any?, Pair<TTFFile, Font>> {
+               return object : ReadOnlyProperty<Any?, Pair<TTFFile, Font>> {
+                       private var loadedFile: TTFFile? = null
                        private var loadedFont: Font? = null
                        private var lastLoaded = Long.MIN_VALUE
                        
                        private val fontFile = fontsRoot.combineSafe("$fontName.ttf")
                        
-                       private fun loadFont(): Font = fontFile.inputStream().use { inStream ->
-                               Font
-                                       .createFont(Font.TRUETYPE_FONT, inStream)
-                                       .deriveFont(DEFAULT_FONT_SIZE)
+                       private fun loadFont(): Pair<TTFFile, Font> {
+                               val bytes = fontFile.readBytes()
+                               
+                               val file = TTFFile(true, true).apply {
+                                       readFont(FontFileReader(ByteArrayInputStream(bytes)))
+                               }
+                               
+                               val font = Font
+                                       .createFont(Font.TRUETYPE_FONT, ByteArrayInputStream(bytes))
+                                       .deriveFont(file.unitsPerEm.toFloat())
+                               
+                               return file to font
                        }
                        
                        private val getValueLock = ReentrantLock(true)
                        
-                       override fun getValue(thisRef: Any?, property: KProperty<*>): Font {
+                       override fun getValue(thisRef: Any?, property: KProperty<*>): Pair<TTFFile, Font> {
                                return getValueLock.withLock {
+                                       val file = loadedFile
                                        val font = loadedFont
                                        val lastMod = fontFile.lastModified()
                                        
-                                       if (font == null || lastLoaded < lastMod)
-                                               loadFont().also {
-                                                       loadedFont = it
+                                       if (file == null || font == null || lastLoaded < lastMod)
+                                               loadFont().also { (file, font) ->
+                                                       loadedFile = file
+                                                       loadedFont = font
                                                        lastLoaded = lastMod
                                                }
-                                       else font
+                                       else file to font
                                }
                        }
                }
@@ -67,56 +81,147 @@ object MechyrdiaSansFont {
        private val mechyrdiaSansBI by loadedFont("mechyrdia-sans-bold-italic")
        
        private val mechyrdiaSansFonts = listOf(::mechyrdiaSans, ::mechyrdiaSansI, ::mechyrdiaSansB, ::mechyrdiaSansBI)
-       private fun getFont(bold: Boolean, italic: Boolean): Font {
+       private fun getFont(bold: Boolean, italic: Boolean): Pair<TTFFile, Font> {
                return mechyrdiaSansFonts[(if (bold) 2 else 0) + (if (italic) 1 else 0)].get()
        }
        
-       private fun layoutText(text: String, font: Font, alignAmount: Double): Shape {
+       private fun TTFFile.getGlyph(cp: Int): Int {
+               return try {
+                       unicodeToGlyph(cp)
+               } catch (ex: IOException) {
+                       65535
+               }
+       }
+       
+       private fun String.toCodePointSequence() = sequence {
+               val l = length
+               var i = 0
+               while (i < l) {
+                       val cp = Character.codePointAt(this@toCodePointSequence, i)
+                       i += if (Character.isSupplementaryCodePoint(cp)) 2 else 1
+                       yield(cp)
+               }
+       }
+       
+       private fun TTFFile.getGlyphs(str: String): GlyphSequence {
+               val length = str.codePointCount(0, str.length)
+               val codeSeq = str.toCodePointSequence()
+               val glyphSeq = codeSeq.map { getGlyph(it) }
+               
+               val codes = codeSeq.iterator().let { iter ->
+                       IntArray(length) {
+                               assert(iter.hasNext())
+                               iter.next()
+                       }
+               }
+               
+               val glyphs = glyphSeq.iterator().let { iter ->
+                       IntArray(length) {
+                               assert(iter.hasNext())
+                               iter.next()
+                       }
+               }
+               
+               return GlyphSequence(IntBuffer.wrap(codes), IntBuffer.wrap(glyphs), null)
+       }
+       
+       private fun TTFFile.getBasicWidths(glyphSequence: GlyphSequence): IntArray {
+               return IntArray(glyphSequence.glyphCount) { i ->
+                       if (i == 0)
+                               mtx[glyphSequence.getGlyph(i)].wx
+                       else {
+                               val prev = glyphSequence.getGlyph(i - 1)
+                               val curr = glyphSequence.getGlyph(i)
+                               (rawKerning[prev]?.get(curr) ?: 0) + mtx[curr].wx
+                       }
+               }
+       }
+       
+       private fun TTFFile.getGlyphPositions(glyphSequence: GlyphSequence, widths: IntArray): Array<IntArray> {
+               val adjustments = Array(glyphSequence.glyphCount) { IntArray(4) }
+               gpos.position(glyphSequence, "latn", "*", 0, widths, adjustments)
+               
+               // I don't know why this is necessary,
+               // but it gives me the results I want.
+               for (adjustment in adjustments) {
+                       adjustment[0] *= 2
+                       adjustment[1] *= 2
+                       adjustment[2] *= 2
+                       adjustment[3] *= 2
+               }
+               
+               return adjustments
+       }
+       
+       private fun getWidth(widths: IntArray, glyphPositions: Array<IntArray>): Int {
+               return widths.zip(glyphPositions) { width, pos -> width + pos[2] }.sum()
+       }
+       
+       private fun layoutText(text: String, file: TTFFile, font: Font, alignAmount: Double): Shape {
                val img = BufferedImage(256, 160, BufferedImage.TYPE_INT_ARGB)
                val g2d = img.createGraphics()
                try {
-                       g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
-                       g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON)
+                       val charHolder = CharArray(2)
+                       val lineHeight = file.rawLowerCaseAscent - file.rawLowerCaseDescent
                        
-                       val fontMetrics = g2d.getFontMetrics(font)
                        val lines = text.split("\r\n", "\n", "\r")
-                       val width = lines.maxOf { fontMetrics.stringWidth(it) }.toDouble()
-                       var y = 0.0
+                       val lineGlyphs = lines.map { file.getGlyphs(it) }
+                       val lineBasics = lineGlyphs.map { file.getBasicWidths(it) }
+                       val lineAdjust = lineGlyphs.zip(lineBasics) { glyphs, widths -> file.getGlyphPositions(glyphs, widths) }
+                       val lineWidths = lineBasics.zip(lineAdjust) { width, adjust -> getWidth(width, adjust) }
+                       val blockWidth = lineWidths.max()
+                       var ly = 0.0
                        
                        yieldThread()
                        
                        val shape = GeneralPath()
                        val tf = AffineTransform()
-                       for (line in lines) {
+                       for ((li, line) in lines.withIndex()) {
                                if (line.isNotBlank()) {
-                                       val x = (width - fontMetrics.stringWidth(line)) * alignAmount
+                                       val lineWidth = lineWidths[li]
+                                       val lx = (blockWidth - lineWidth) * alignAmount
                                        
-                                       // Mechyrdia Sans only supports left-to-right scripts, so we can ignore bidirectional text
-                                       val glyphs = font.layoutGlyphVector(g2d.fontRenderContext, line.toCharArray(), 0, line.length, Font.LAYOUT_LEFT_TO_RIGHT)
-                                       val textShape = glyphs.outline as GeneralPath
+                                       var cx = 0
+                                       var cy = 0
                                        
-                                       tf.setToIdentity()
-                                       tf.translate(x, y)
-                                       shape.append(textShape.getPathIterator(tf), false)
+                                       val basicAdv = lineBasics[li]
+                                       val adjusted = lineAdjust[li]
+                                       val glyphSeq = lineGlyphs[li]
+                                       for ((ci, codePoint) in glyphSeq.getCharacterArray(false).withIndex()) {
+                                               val length = Character.toChars(codePoint, charHolder, 0)
+                                               val glyph = font.layoutGlyphVector(g2d.fontRenderContext, charHolder, 0, length, Font.LAYOUT_LEFT_TO_RIGHT)
+                                               val glyphShape = glyph.outline as GeneralPath
+                                               val glyphShift = adjusted[ci]
+                                               
+                                               tf.setToIdentity()
+                                               tf.translate(lx + cx + glyphShift[0], ly + cy + glyphShift[1])
+                                               shape.append(glyphShape.getPathIterator(tf), false)
+                                               
+                                               cx += glyphShift[2] + basicAdv[ci]
+                                               cy += glyphShift[3]
+                                       }
                                }
                                
-                               y += fontMetrics.height
+                               ly += lineHeight
                                
                                yieldThread()
                        }
                        
                        return shape
+               } catch (ex: Exception) {
+                       ex.printStackTrace()
+                       return GeneralPath()
                } finally {
                        g2d.dispose()
                }
        }
        
-       private fun Shape.toSvgDocument(): String {
+       private fun Shape.toSvgDocument(scale: Double): String {
                return buildString {
                        appendLine("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
                        
                        val viewBox = bounds2D
-                       appendLine("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"${viewBox.width * 2}\" height=\"${viewBox.height * 2}\" viewBox=\"${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}\">")
+                       appendLine("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"${viewBox.width * scale}\" height=\"${viewBox.height * scale}\" viewBox=\"${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}\">")
                        appendLine(toSvgPath())
                        appendLine("</svg>")
                }