<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/externals" />
+ <option value="$PROJECT_DIR$/fontparser" />
</set>
</option>
</GradleProjectSettings>
implementation("org.slf4j:slf4j-api:2.0.7")
implementation("ch.qos.logback:logback-classic:1.4.14")
+ implementation(project(":fontparser"))
//implementation(project(":fightgame"))
}
}
--- /dev/null
+
+ 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
--- /dev/null
+plugins {
+ java
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(17))
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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() + " }";
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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
+ };
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
+
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+ }
+
+}
+
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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'
+ };
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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() {
+ }
+}
--- /dev/null
+/*
+ * 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() {
+ }
+}
--- /dev/null
+/*
+ * 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();
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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("foo.txt");
+ * // 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("foo.txt");
+ * // 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 > 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 > 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 >= 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 >= 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 >= 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 >= 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 >= 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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());
+ }
+ }
+ }
+
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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.
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+
+}
--- /dev/null
+/*
+ * 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;
+
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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("<");
+ } else if (c == '>') {
+ sb.append(">");
+ } else if (c == '&') {
+ sb.append("&");
+ } 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;
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
rootProject.name = "factbooks"
include("externals")
+include("fontparser")
//include("fightgame")
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) {
}
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
}
}
}
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>")
}