From: Lanius Trolling Date: Sun, 3 Mar 2024 13:29:29 +0000 (-0500) Subject: Fix Mechyrdia Sans kerning X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=0107d8f04892e648a8acd10637fcebbabf5413b0;p=factbooks Fix Mechyrdia Sans kerning --- diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 4090d4d..08d6d55 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -10,6 +10,7 @@ diff --git a/build.gradle.kts b/build.gradle.kts index de729a9..b2ec0e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -142,6 +142,7 @@ kotlin { implementation("org.slf4j:slf4j-api:2.0.7") implementation("ch.qos.logback:logback-classic:1.4.14") + implementation(project(":fontparser")) //implementation(project(":fightgame")) } } diff --git a/fontparser/LICENSE b/fontparser/LICENSE new file mode 100644 index 0000000..f1c107f --- /dev/null +++ b/fontparser/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 Jared Rummler + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/fontparser/build.gradle.kts b/fontparser/build.gradle.kts new file mode 100644 index 0000000..ac45f8c --- /dev/null +++ b/fontparser/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + java +} + +repositories { + mavenCentral() +} + +dependencies { +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiClass.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiClass.java new file mode 100644 index 0000000..8c36ade --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiClass.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 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. + * + */ + +/* $Id: License.java 1039179 2010-11-25 21:04:09Z vhennebert $ */ + +package com.jaredrummler.fontreader.complexscripts.bidi; + +import java.util.Arrays; + +/** + * Bidirectional class utilities. + */ +public final class BidiClass { + + private BidiClass() { + } + + private static final byte[] BC_L_1 = { + 15, 15, 15, 15, 15, 15, 15, 15, 15, 17, 16, 17, 18, 16, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 16, 16, 16, 17, 18, 19, 19, 11, 11, 11, 19, 19, 19, 19, 19, 10, 13, 10, 13, 13, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 13, + 19, 19, 19, 19, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 19, 19, + 19, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 19, 19, 19, 15, 15, + 15, 15, 15, 15, 16, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 13, 19, 11, 11, 11, 11, 19, 19, 19, 19, 1, 19, 19, 15, 19, 19, 11, 11, 9, 9, 19, 1, 19, 19, 19, 9, 1, + 19, 19, 19, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 1, 1, 1, 1, 1, 1, 1 + }; + + private static final byte[] BC_R_1 = { + 4, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 4, 14, 4, 14, 14, 4, 14, 14, 4, 14, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 12, 12, 12, 12, 5, 5, 19, 19, 5, 11, 11, 5, 13, 5, 19, 19, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 11, 12, 12, 5, 5, 5, 14, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 14, 14, 14, 14, 14, 14, 14, 12, 19, 14, 14, 14, 14, + 14, 14, 5, 5, 14, 14, 19, 14, 14, 14, 14, 5, 5, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 5, 5, 5, 5, 5, 5 + }; + + private static final int[] BC_S_1 = { + 256, 443, 444, 448, 452, 660, 661, 688, 697, 699, 706, 710, 720, 722, 736, 741, 748, 749, 750, 751, 768, 880, 884, + 885, 886, 890, 891, 894, 900, 902, 903, 904, 908, 910, 931, 1014, 1015, 1154, 1155, 1160, 1162, 1329, 1369, 1370, + 1377, 1417, 1418, 1792, 1806, 1807, 1808, 1809, 1810, 1840, 1867, 1869, 1958, 1969, 1970, 1984, 1994, 2027, 2036, + 2038, 2039, 2042, 2043, 2048, 2070, 2074, 2075, 2084, 2085, 2088, 2089, 2094, 2096, 2111, 2112, 2137, 2140, 2142, + 2143, 2304, 2307, 2308, 2362, 2363, 2364, 2365, 2366, 2369, 2377, 2381, 2382, 2384, 2385, 2392, 2402, 2404, 2406, + 2416, 2417, 2418, 2425, 2433, 2434, 2437, 2447, 2451, 2474, 2482, 2486, 2492, 2493, 2494, 2497, 2503, 2507, 2509, + 2510, 2519, 2524, 2527, 2530, 2534, 2544, 2546, 2548, 2554, 2555, 2561, 2563, 2565, 2575, 2579, 2602, 2610, 2613, + 2616, 2620, 2622, 2625, 2631, 2635, 2641, 2649, 2654, 2662, 2672, 2674, 2677, 2689, 2691, 2693, 2703, 2707, 2730, + 2738, 2741, 2748, 2749, 2750, 2753, 2759, 2761, 2763, 2765, 2768, 2784, 2786, 2790, 2801, 2817, 2818, 2821, 2831, + 2835, 2858, 2866, 2869, 2876, 2877, 2878, 2879, 2880, 2881, 2887, 2891, 2893, 2902, 2903, 2908, 2911, 2914, 2918, + 2928, 2929, 2930, 2946, 2947, 2949, 2958, 2962, 2969, 2972, 2974, 2979, 2984, 2990, 3006, 3008, 3009, 3014, 3018, + 3021, 3024, 3031, 3046, 3056, 3059, 3065, 3066, 3073, 3077, 3086, 3090, 3114, 3125, 3133, 3134, 3137, 3142, 3146, + 3157, 3160, 3168, 3170, 3174, 3192, 3199, 3202, 3205, 3214, 3218, 3242, 3253, 3260, 3261, 3262, 3263, 3264, 3270, + 3271, 3274, 3276, 3285, 3294, 3296, 3298, 3302, 3313, 3330, 3333, 3342, 3346, 3389, 3390, 3393, 3398, 3402, 3405, + 3406, 3415, 3424, 3426, 3430, 3440, 3449, 3450, 3458, 3461, 3482, 3507, 3517, 3520, 3530, 3535, 3538, 3542, 3544, + 3570, 3572, 3585, 3633, 3634, 3636, 3647, 3648, 3654, 3655, 3663, 3664, 3674, 3713, 3716, 3719, 3722, 3725, 3732, + 3737, 3745, 3749, 3751, 3754, 3757, 3761, 3762, 3764, 3771, 3773, 3776, 3782, 3784, 3792, 3804, 3840, 3841, 3844, + 3859, 3864, 3866, 3872, 3882, 3892, 3893, 3894, 3895, 3896, 3897, 3898, 3899, 3900, 3901, 3902, 3904, 3913, 3953, + 3967, 3968, 3973, 3974, 3976, 3981, 3993, 4030, 4038, 4039, 4046, 4048, 4053, 4057, 4096, 4139, 4141, 4145, 4146, + 4152, 4153, 4155, 4157, 4159, 4160, 4170, 4176, 4182, 4184, 4186, 4190, 4193, 4194, 4197, 4199, 4206, 4209, 4213, + 4226, 4227, 4229, 4231, 4237, 4238, 4239, 4240, 4250, 4253, 4254, 4256, 4304, 4347, 4348, 4352, 4682, 4688, 4696, + 4698, 4704, 4746, 4752, 4786, 4792, 4800, 4802, 4808, 4824, 4882, 4888, 4957, 4960, 4961, 4969, 4992, 5008, 5024, + 5120, 5121, 5741, 5743, 5760, 5761, 5787, 5788, 5792, 5867, 5870, 5888, 5902, 5906, 5920, 5938, 5941, 5952, 5970, + 5984, 5998, 6002, 6016, 6068, 6070, 6071, 6078, 6086, 6087, 6089, 6100, 6103, 6104, 6107, 6108, 6109, 6112, 6128, + 6144, 6150, 6151, 6155, 6158, 6160, 6176, 6211, 6212, 6272, 6313, 6314, 6320, 6400, 6432, 6435, 6439, 6441, 6448, + 6450, 6451, 6457, 6464, 6468, 6470, 6480, 6512, 6528, 6576, 6593, 6600, 6608, 6618, 6622, 6656, 6679, 6681, 6686, + 6688, 6741, 6742, 6743, 6744, 6752, 6753, 6754, 6755, 6757, 6765, 6771, 6783, 6784, 6800, 6816, 6823, 6824, 6912, + 6916, 6917, 6964, 6965, 6966, 6971, 6972, 6973, 6978, 6979, 6981, 6992, 7002, 7009, 7019, 7028, 7040, 7042, 7043, + 7073, 7074, 7078, 7080, 7082, 7086, 7088, 7104, 7142, 7143, 7144, 7146, 7149, 7150, 7151, 7154, 7164, 7168, 7204, + 7212, 7220, 7222, 7227, 7232, 7245, 7248, 7258, 7288, 7294, 7376, 7379, 7380, 7393, 7394, 7401, 7405, 7406, 7410, + 7424, 7468, 7522, 7544, 7545, 7579, 7616, 7676, 7680, 7960, 7968, 8008, 8016, 8025, 8027, 8029, 8031, 8064, 8118, + 8125, 8126, 8127, 8130, 8134, 8141, 8144, 8150, 8157, 8160, 8173, 8178, 8182, 8189, 8192, 8203, 8206, 8207, 8208, + 8214, 8216, 8217, 8218, 8219, 8221, 8222, 8223, 8224, 8232, 8233, 8234, 8235, 8236, 8237, 8238, 8239, 8240, 8245, + 8249, 8250, 8251, 8255, 8257, 8260, 8261, 8262, 8263, 8274, 8275, 8276, 8277, 8287, 8288, 8293, 8298, 8304, 8305, + 8308, 8314, 8316, 8317, 8318, 8319, 8320, 8330, 8332, 8333, 8334, 8336, 8352, 8400, 8413, 8417, 8418, 8421, 8448, + 8450, 8451, 8455, 8456, 8458, 8468, 8469, 8470, 8472, 8473, 8478, 8484, 8485, 8486, 8487, 8488, 8489, 8490, 8494, + 8495, 8501, 8505, 8506, 8508, 8512, 8517, 8522, 8523, 8524, 8526, 8527, 8528, 8544, 8579, 8581, 8585, 8592, 8597, + 8602, 8604, 8608, 8609, 8611, 8612, 8614, 8615, 8622, 8623, 8654, 8656, 8658, 8659, 8660, 8661, 8692, 8722, 8723, + 8724, 8960, 8968, 8972, 8992, 8994, 9001, 9002, 9003, 9014, 9083, 9084, 9085, 9109, 9110, 9115, 9140, 9180, 9186, + 9216, 9280, 9312, 9352, 9372, 9450, 9472, 9655, 9656, 9665, 9666, 9720, 9728, 9839, 9840, 9900, 9901, 9985, 10088, + 10089, 10090, 10091, 10092, 10093, 10094, 10095, 10096, 10097, 10098, 10099, 10100, 10101, 10102, 10132, 10176, + 10181, 10182, 10183, 10188, 10190, 10214, 10215, 10216, 10217, 10218, 10219, 10220, 10221, 10222, 10223, 10224, + 10240, 10496, 10627, 10628, 10629, 10630, 10631, 10632, 10633, 10634, 10635, 10636, 10637, 10638, 10639, 10640, + 10641, 10642, 10643, 10644, 10645, 10646, 10647, 10648, 10649, 10712, 10713, 10714, 10715, 10716, 10748, 10749, + 10750, 11008, 11056, 11077, 11079, 11088, 11264, 11312, 11360, 11389, 11390, 11493, 11499, 11503, 11513, 11517, + 11518, 11520, 11568, 11631, 11632, 11647, 11648, 11680, 11688, 11696, 11704, 11712, 11720, 11728, 11736, 11744, + 11776, 11778, 11779, 11780, 11781, 11782, 11785, 11786, 11787, 11788, 11789, 11790, 11799, 11800, 11802, 11803, + 11804, 11805, 11806, 11808, 11809, 11810, 11811, 11812, 11813, 11814, 11815, 11816, 11817, 11818, 11823, 11824, + 11904, 11931, 12032, 12272, 12288, 12289, 12292, 12293, 12294, 12295, 12296, 12297, 12298, 12299, 12300, 12301, + 12302, 12303, 12304, 12305, 12306, 12308, 12309, 12310, 12311, 12312, 12313, 12314, 12315, 12316, 12317, 12318, + 12320, 12321, 12330, 12336, 12337, 12342, 12344, 12347, 12348, 12349, 12350, 12353, 12441, 12443, 12445, 12447, + 12448, 12449, 12539, 12540, 12543, 12549, 12593, 12688, 12690, 12694, 12704, 12736, 12784, 12800, 12829, 12832, + 12842, 12880, 12881, 12896, 12924, 12927, 12928, 12938, 12977, 12992, 13004, 13008, 13056, 13175, 13179, 13278, + 13280, 13311, 13312, 19904, 19968, 40960, 40981, 40982, 42128, 42192, 42232, 42238, 42240, 42508, 42509, 42512, + 42528, 42538, 42560, 42606, 42607, 42608, 42611, 42620, 42622, 42623, 42624, 42656, 42726, 42736, 42738, 42752, + 42775, 42784, 42786, 42864, 42865, 42888, 42889, 42891, 42896, 42912, 43002, 43003, 43010, 43011, 43014, 43015, + 43019, 43020, 43043, 43045, 43047, 43048, 43056, 43062, 43064, 43065, 43072, 43124, 43136, 43138, 43188, 43204, + 43214, 43216, 43232, 43250, 43256, 43259, 43264, 43274, 43302, 43310, 43312, 43335, 43346, 43359, 43360, 43392, + 43395, 43396, 43443, 43444, 43446, 43450, 43452, 43453, 43457, 43471, 43472, 43486, 43520, 43561, 43567, 43569, + 43571, 43573, 43584, 43587, 43588, 43596, 43597, 43600, 43612, 43616, 43632, 43633, 43639, 43642, 43643, 43648, + 43696, 43697, 43698, 43701, 43703, 43705, 43710, 43712, 43713, 43714, 43739, 43741, 43742, 43777, 43785, 43793, + 43808, 43816, 43968, 44003, 44005, 44006, 44008, 44009, 44011, 44012, 44013, 44016, 44032, 55216, 55243, 57344, + 63744, 64048, 64112, 64256, 64275, 64285, 64286, 64287, 64297, 64298, 64311, 64312, 64317, 64318, 64319, 64320, + 64322, 64323, 64325, 64326, 64336, 64434, 64450, 64467, 64830, 64831, 64832, 64848, 64912, 64914, 64968, 64976, + 65008, 65020, 65021, 65022, 65024, 65040, 65047, 65048, 65049, 65056, 65072, 65073, 65075, 65077, 65078, 65079, + 65080, 65081, 65082, 65083, 65084, 65085, 65086, 65087, 65088, 65089, 65090, 65091, 65092, 65093, 65095, 65096, + 65097, 65101, 65104, 65105, 65106, 65108, 65109, 65110, 65112, 65113, 65114, 65115, 65116, 65117, 65118, 65119, + 65120, 65122, 65123, 65124, 65128, 65129, 65130, 65131, 65136, 65141, 65142, 65277, 65279, 65281, 65283, 65284, + 65285, 65286, 65288, 65289, 65290, 65291, 65292, 65293, 65294, 65296, 65306, 65307, 65308, 65311, 65313, 65339, + 65340, 65341, 65342, 65343, 65344, 65345, 65371, 65372, 65373, 65374, 65375, 65376, 65377, 65378, 65379, 65380, + 65382, 65392, 65393, 65438, 65440, 65474, 65482, 65490, 65498, 65504, 65506, 65507, 65508, 65509, 65512, 65513, + 65517, 65520, 65529, 65532, 65534, 65536, 65549, 65576, 65596, 65599, 65616, 65664, 65792, 65793, 65794, 65799, + 65847, 65856, 65909, 65913, 65930, 65936, 66000, 66045, 66176, 66208, 66304, 66336, 66352, 66369, 66370, 66378, + 66432, 66463, 66464, 66504, 66512, 66513, 66560, 66640, 66720, 67584, 67590, 67592, 67593, 67594, 67638, 67639, + 67641, 67644, 67645, 67647, 67670, 67671, 67672, 67680, 67840, 67862, 67868, 67871, 67872, 67898, 67903, 67904, + 68096, 68097, 68100, 68101, 68103, 68108, 68112, 68116, 68117, 68120, 68121, 68148, 68152, 68155, 68159, 68160, + 68168, 68176, 68185, 68192, 68221, 68223, 68224, 68352, 68406, 68409, 68416, 68438, 68440, 68448, 68467, 68472, + 68480, 68608, 68681, 69216, 69247, 69632, 69633, 69634, 69635, 69688, 69703, 69714, 69734, 69760, 69762, 69763, + 69808, 69811, 69815, 69817, 69819, 69821, 69822, 73728, 74752, 74864, 77824, 92160, 110592, 118784, 119040, + 119081, 119141, 119143, 119146, 119149, 119155, 119163, 119171, 119173, 119180, 119210, 119214, 119296, 119362, + 119365, 119552, 119648, 119808, 119894, 119966, 119970, 119973, 119977, 119982, 119995, 119997, 120005, 120071, + 120077, 120086, 120094, 120123, 120128, 120134, 120138, 120146, 120488, 120513, 120514, 120539, 120540, 120571, + 120572, 120597, 120598, 120629, 120630, 120655, 120656, 120687, 120688, 120713, 120714, 120745, 120746, 120771, + 120772, 120782, 124928, 126976, 127024, 127136, 127153, 127169, 127185, 127232, 127248, 127280, 127344, 127462, + 127504, 127552, 127568, 127744, 127792, 127799, 127872, 127904, 127942, 127968, 128000, 128064, 128066, 128140, + 128141, 128249, 128256, 128292, 128293, 128336, 128507, 128513, 128530, 128534, 128536, 128538, 128540, 128544, + 128552, 128557, 128560, 128565, 128581, 128640, 128768, 131070, 131072, 173824, 177984, 194560, 196606, 262142, + 327678, 393214, 458750, 524286, 589822, 655358, 720894, 786430, 851966, 917502, 917505, 917506, 917536, 917632, + 917760, 918000, 983038, 983040, 1048574, 1048576, 1114110 + }; + + private static final int[] BC_E_1 = { + 442, 443, 447, 451, 659, 660, 687, 696, 698, 705, 709, 719, 721, 735, 740, 747, 748, 749, 750, 767, 879, 883, 884, + 885, 887, 890, 893, 894, 901, 902, 903, 906, 908, 929, 1013, 1014, 1153, 1154, 1159, 1161, 1319, 1366, 1369, 1375, + 1415, 1417, 1418, 1805, 1806, 1807, 1808, 1809, 1839, 1866, 1868, 1957, 1968, 1969, 1983, 1993, 2026, 2035, 2037, + 2038, 2041, 2042, 2047, 2069, 2073, 2074, 2083, 2084, 2087, 2088, 2093, 2095, 2110, 2111, 2136, 2139, 2141, 2142, + 2303, 2306, 2307, 2361, 2362, 2363, 2364, 2365, 2368, 2376, 2380, 2381, 2383, 2384, 2391, 2401, 2403, 2405, 2415, + 2416, 2417, 2423, 2431, 2433, 2435, 2444, 2448, 2472, 2480, 2482, 2489, 2492, 2493, 2496, 2500, 2504, 2508, 2509, + 2510, 2519, 2525, 2529, 2531, 2543, 2545, 2547, 2553, 2554, 2555, 2562, 2563, 2570, 2576, 2600, 2608, 2611, 2614, + 2617, 2620, 2624, 2626, 2632, 2637, 2641, 2652, 2654, 2671, 2673, 2676, 2677, 2690, 2691, 2701, 2705, 2728, 2736, + 2739, 2745, 2748, 2749, 2752, 2757, 2760, 2761, 2764, 2765, 2768, 2785, 2787, 2799, 2801, 2817, 2819, 2828, 2832, + 2856, 2864, 2867, 2873, 2876, 2877, 2878, 2879, 2880, 2884, 2888, 2892, 2893, 2902, 2903, 2909, 2913, 2915, 2927, + 2928, 2929, 2935, 2946, 2947, 2954, 2960, 2965, 2970, 2972, 2975, 2980, 2986, 3001, 3007, 3008, 3010, 3016, 3020, + 3021, 3024, 3031, 3055, 3058, 3064, 3065, 3066, 3075, 3084, 3088, 3112, 3123, 3129, 3133, 3136, 3140, 3144, 3149, + 3158, 3161, 3169, 3171, 3183, 3198, 3199, 3203, 3212, 3216, 3240, 3251, 3257, 3260, 3261, 3262, 3263, 3268, 3270, + 3272, 3275, 3277, 3286, 3294, 3297, 3299, 3311, 3314, 3331, 3340, 3344, 3386, 3389, 3392, 3396, 3400, 3404, 3405, + 3406, 3415, 3425, 3427, 3439, 3445, 3449, 3455, 3459, 3478, 3505, 3515, 3517, 3526, 3530, 3537, 3540, 3542, 3551, + 3571, 3572, 3632, 3633, 3635, 3642, 3647, 3653, 3654, 3662, 3663, 3673, 3675, 3714, 3716, 3720, 3722, 3725, 3735, + 3743, 3747, 3749, 3751, 3755, 3760, 3761, 3763, 3769, 3772, 3773, 3780, 3782, 3789, 3801, 3805, 3840, 3843, 3858, + 3863, 3865, 3871, 3881, 3891, 3892, 3893, 3894, 3895, 3896, 3897, 3898, 3899, 3900, 3901, 3903, 3911, 3948, 3966, + 3967, 3972, 3973, 3975, 3980, 3991, 4028, 4037, 4038, 4044, 4047, 4052, 4056, 4058, 4138, 4140, 4144, 4145, 4151, + 4152, 4154, 4156, 4158, 4159, 4169, 4175, 4181, 4183, 4185, 4189, 4192, 4193, 4196, 4198, 4205, 4208, 4212, 4225, + 4226, 4228, 4230, 4236, 4237, 4238, 4239, 4249, 4252, 4253, 4255, 4293, 4346, 4347, 4348, 4680, 4685, 4694, 4696, + 4701, 4744, 4749, 4784, 4789, 4798, 4800, 4805, 4822, 4880, 4885, 4954, 4959, 4960, 4968, 4988, 5007, 5017, 5108, + 5120, 5740, 5742, 5759, 5760, 5786, 5787, 5788, 5866, 5869, 5872, 5900, 5905, 5908, 5937, 5940, 5942, 5969, 5971, + 5996, 6000, 6003, 6067, 6069, 6070, 6077, 6085, 6086, 6088, 6099, 6102, 6103, 6106, 6107, 6108, 6109, 6121, 6137, + 6149, 6150, 6154, 6157, 6158, 6169, 6210, 6211, 6263, 6312, 6313, 6314, 6389, 6428, 6434, 6438, 6440, 6443, 6449, + 6450, 6456, 6459, 6464, 6469, 6479, 6509, 6516, 6571, 6592, 6599, 6601, 6617, 6618, 6655, 6678, 6680, 6683, 6687, + 6740, 6741, 6742, 6743, 6750, 6752, 6753, 6754, 6756, 6764, 6770, 6780, 6783, 6793, 6809, 6822, 6823, 6829, 6915, + 6916, 6963, 6964, 6965, 6970, 6971, 6972, 6977, 6978, 6980, 6987, 7001, 7008, 7018, 7027, 7036, 7041, 7042, 7072, + 7073, 7077, 7079, 7081, 7082, 7087, 7097, 7141, 7142, 7143, 7145, 7148, 7149, 7150, 7153, 7155, 7167, 7203, 7211, + 7219, 7221, 7223, 7231, 7241, 7247, 7257, 7287, 7293, 7295, 7378, 7379, 7392, 7393, 7400, 7404, 7405, 7409, 7410, + 7467, 7521, 7543, 7544, 7578, 7615, 7654, 7679, 7957, 7965, 8005, 8013, 8023, 8025, 8027, 8029, 8061, 8116, 8124, + 8125, 8126, 8129, 8132, 8140, 8143, 8147, 8155, 8159, 8172, 8175, 8180, 8188, 8190, 8202, 8205, 8206, 8207, 8213, + 8215, 8216, 8217, 8218, 8220, 8221, 8222, 8223, 8231, 8232, 8233, 8234, 8235, 8236, 8237, 8238, 8239, 8244, 8248, + 8249, 8250, 8254, 8256, 8259, 8260, 8261, 8262, 8273, 8274, 8275, 8276, 8286, 8287, 8292, 8297, 8303, 8304, 8305, + 8313, 8315, 8316, 8317, 8318, 8319, 8329, 8331, 8332, 8333, 8334, 8348, 8377, 8412, 8416, 8417, 8420, 8432, 8449, + 8450, 8454, 8455, 8457, 8467, 8468, 8469, 8471, 8472, 8477, 8483, 8484, 8485, 8486, 8487, 8488, 8489, 8493, 8494, + 8500, 8504, 8505, 8507, 8511, 8516, 8521, 8522, 8523, 8525, 8526, 8527, 8543, 8578, 8580, 8584, 8585, 8596, 8601, + 8603, 8607, 8608, 8610, 8611, 8613, 8614, 8621, 8622, 8653, 8655, 8657, 8658, 8659, 8660, 8691, 8721, 8722, 8723, + 8959, 8967, 8971, 8991, 8993, 9000, 9001, 9002, 9013, 9082, 9083, 9084, 9108, 9109, 9114, 9139, 9179, 9185, 9203, + 9254, 9290, 9351, 9371, 9449, 9471, 9654, 9655, 9664, 9665, 9719, 9727, 9838, 9839, 9899, 9900, 9983, 10087, + 10088, 10089, 10090, 10091, 10092, 10093, 10094, 10095, 10096, 10097, 10098, 10099, 10100, 10101, 10131, 10175, + 10180, 10181, 10182, 10186, 10188, 10213, 10214, 10215, 10216, 10217, 10218, 10219, 10220, 10221, 10222, 10223, + 10239, 10495, 10626, 10627, 10628, 10629, 10630, 10631, 10632, 10633, 10634, 10635, 10636, 10637, 10638, 10639, + 10640, 10641, 10642, 10643, 10644, 10645, 10646, 10647, 10648, 10711, 10712, 10713, 10714, 10715, 10747, 10748, + 10749, 11007, 11055, 11076, 11078, 11084, 11097, 11310, 11358, 11388, 11389, 11492, 11498, 11502, 11505, 11516, + 11517, 11519, 11557, 11621, 11631, 11632, 11647, 11670, 11686, 11694, 11702, 11710, 11718, 11726, 11734, 11742, + 11775, 11777, 11778, 11779, 11780, 11781, 11784, 11785, 11786, 11787, 11788, 11789, 11798, 11799, 11801, 11802, + 11803, 11804, 11805, 11807, 11808, 11809, 11810, 11811, 11812, 11813, 11814, 11815, 11816, 11817, 11822, 11823, + 11825, 11929, 12019, 12245, 12283, 12288, 12291, 12292, 12293, 12294, 12295, 12296, 12297, 12298, 12299, 12300, + 12301, 12302, 12303, 12304, 12305, 12307, 12308, 12309, 12310, 12311, 12312, 12313, 12314, 12315, 12316, 12317, + 12319, 12320, 12329, 12335, 12336, 12341, 12343, 12346, 12347, 12348, 12349, 12351, 12438, 12442, 12444, 12446, + 12447, 12448, 12538, 12539, 12542, 12543, 12589, 12686, 12689, 12693, 12703, 12730, 12771, 12799, 12828, 12830, + 12841, 12879, 12880, 12895, 12923, 12926, 12927, 12937, 12976, 12991, 13003, 13007, 13054, 13174, 13178, 13277, + 13279, 13310, 13311, 19893, 19967, 40907, 40980, 40981, 42124, 42182, 42231, 42237, 42239, 42507, 42508, 42511, + 42527, 42537, 42539, 42605, 42606, 42607, 42610, 42611, 42621, 42622, 42623, 42647, 42725, 42735, 42737, 42743, + 42774, 42783, 42785, 42863, 42864, 42887, 42888, 42890, 42894, 42897, 42921, 43002, 43009, 43010, 43013, 43014, + 43018, 43019, 43042, 43044, 43046, 43047, 43051, 43061, 43063, 43064, 43065, 43123, 43127, 43137, 43187, 43203, + 43204, 43215, 43225, 43249, 43255, 43258, 43259, 43273, 43301, 43309, 43311, 43334, 43345, 43347, 43359, 43388, + 43394, 43395, 43442, 43443, 43445, 43449, 43451, 43452, 43456, 43469, 43471, 43481, 43487, 43560, 43566, 43568, + 43570, 43572, 43574, 43586, 43587, 43595, 43596, 43597, 43609, 43615, 43631, 43632, 43638, 43641, 43642, 43643, + 43695, 43696, 43697, 43700, 43702, 43704, 43709, 43711, 43712, 43713, 43714, 43740, 43741, 43743, 43782, 43790, + 43798, 43814, 43822, 44002, 44004, 44005, 44007, 44008, 44010, 44011, 44012, 44013, 44025, 55203, 55238, 55291, + 63743, 64045, 64109, 64217, 64262, 64279, 64285, 64286, 64296, 64297, 64310, 64311, 64316, 64317, 64318, 64319, + 64321, 64322, 64324, 64325, 64335, 64433, 64449, 64466, 64829, 64830, 64831, 64847, 64911, 64913, 64967, 64975, + 65007, 65019, 65020, 65021, 65023, 65039, 65046, 65047, 65048, 65049, 65062, 65072, 65074, 65076, 65077, 65078, + 65079, 65080, 65081, 65082, 65083, 65084, 65085, 65086, 65087, 65088, 65089, 65090, 65091, 65092, 65094, 65095, + 65096, 65100, 65103, 65104, 65105, 65106, 65108, 65109, 65111, 65112, 65113, 65114, 65115, 65116, 65117, 65118, + 65119, 65121, 65122, 65123, 65126, 65128, 65129, 65130, 65131, 65140, 65141, 65276, 65278, 65279, 65282, 65283, + 65284, 65285, 65287, 65288, 65289, 65290, 65291, 65292, 65293, 65295, 65305, 65306, 65307, 65310, 65312, 65338, + 65339, 65340, 65341, 65342, 65343, 65344, 65370, 65371, 65372, 65373, 65374, 65375, 65376, 65377, 65378, 65379, + 65381, 65391, 65392, 65437, 65439, 65470, 65479, 65487, 65495, 65500, 65505, 65506, 65507, 65508, 65510, 65512, + 65516, 65518, 65528, 65531, 65533, 65535, 65547, 65574, 65594, 65597, 65613, 65629, 65786, 65792, 65793, 65794, + 65843, 65855, 65908, 65912, 65929, 65930, 65947, 66044, 66045, 66204, 66256, 66334, 66339, 66368, 66369, 66377, + 66378, 66461, 66463, 66499, 66511, 66512, 66517, 66639, 66717, 66729, 67589, 67591, 67592, 67593, 67637, 67638, + 67640, 67643, 67644, 67646, 67669, 67670, 67671, 67679, 67839, 67861, 67867, 67870, 67871, 67897, 67902, 67903, + 68095, 68096, 68099, 68100, 68102, 68107, 68111, 68115, 68116, 68119, 68120, 68147, 68151, 68154, 68158, 68159, + 68167, 68175, 68184, 68191, 68220, 68222, 68223, 68351, 68405, 68408, 68415, 68437, 68439, 68447, 68466, 68471, + 68479, 68607, 68680, 69215, 69246, 69631, 69632, 69633, 69634, 69687, 69702, 69709, 69733, 69743, 69761, 69762, + 69807, 69810, 69814, 69816, 69818, 69820, 69821, 69825, 74606, 74850, 74867, 78894, 92728, 110593, 119029, 119078, + 119140, 119142, 119145, 119148, 119154, 119162, 119170, 119172, 119179, 119209, 119213, 119261, 119361, 119364, + 119365, 119638, 119665, 119892, 119964, 119967, 119970, 119974, 119980, 119993, 119995, 120003, 120069, 120074, + 120084, 120092, 120121, 120126, 120132, 120134, 120144, 120485, 120512, 120513, 120538, 120539, 120570, 120571, + 120596, 120597, 120628, 120629, 120654, 120655, 120686, 120687, 120712, 120713, 120744, 120745, 120770, 120771, + 120779, 120831, 126975, 127019, 127123, 127150, 127166, 127183, 127199, 127242, 127278, 127337, 127386, 127490, + 127546, 127560, 127569, 127776, 127797, 127868, 127891, 127940, 127946, 127984, 128062, 128064, 128139, 128140, + 128247, 128252, 128291, 128292, 128317, 128359, 128511, 128528, 128532, 128534, 128536, 128538, 128542, 128549, + 128555, 128557, 128563, 128576, 128591, 128709, 128883, 131071, 173782, 177972, 178205, 195101, 196607, 262143, + 327679, 393215, 458751, 524287, 589823, 655359, 720895, 786431, 851967, 917504, 917505, 917535, 917631, 917759, + 917999, 921599, 983039, 1048573, 1048575, 1114109, 1114111 + }; + + private static final byte[] BC_C_1 = { + 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 19, 19, 1, 19, 1, 19, 19, 19, 1, 19, 14, 1, 19, 19, 1, 1, 1, 19, 19, 1, 19, 1, 1, + 1, 1, 19, 1, 1, 14, 14, 1, 1, 1, 1, 1, 1, 19, 5, 5, 12, 5, 14, 5, 14, 5, 5, 14, 5, 5, 4, 4, 14, 4, 19, 19, 4, 4, + 4, 14, 4, 14, 4, 14, 4, 14, 4, 4, 4, 4, 14, 4, 4, 4, 14, 1, 1, 14, 1, 14, 1, 1, 14, 1, 14, 1, 1, 14, 1, 14, 1, 1, + 1, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 14, 1, 1, 14, 1, 1, 1, 1, 14, 1, 1, 11, 1, 1, 11, 14, 1, 1, 1, 1, + 1, 1, 1, 1, 14, 1, 14, 14, 14, 14, 1, 1, 1, 14, 1, 14, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 14, 14, 1, 1, 14, 1, 1, + 14, 1, 11, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 14, 1, 14, 1, 1, 14, 14, 1, 1, 1, 14, 1, 1, 1, 1, 14, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 14, 1, 1, 1, 14, 1, 1, 1, 1, 19, 11, 19, 1, 1, 1, 1, 1, 1, 1, 14, 1, 14, 14, 14, 1, 1, 14, 1, + 19, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 14, 1, 1, 1, + 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 14, 1, 14, 14, 1, 1, 1, 1, 14, 1, 14, 11, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 14, 1, 14, 14, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 14, 1, 1, 1, 1, 14, 1, 14, 1, 14, 19, 19, 19, 19, + 1, 1, 1, 14, 1, 14, 1, 14, 1, 14, 14, 1, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 14, 1, + 14, 1, 1, 1, 1, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 14, 1, 1, 1, 1, 19, 1, 19, 1, 1, 1, 18, 1, 19, 19, 1, 1, 1, 1, 1, 14, 1, 14, 1, 1, 14, 1, 1, 14, 1, 1, 1, + 14, 1, 14, 1, 14, 1, 1, 1, 11, 1, 14, 1, 19, 19, 19, 19, 14, 18, 1, 1, 1, 1, 1, 14, 1, 1, 1, 14, 1, 14, 1, 1, 14, + 1, 14, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 14, 1, 1, 1, 1, 14, 1, 14, 14, 1, 14, 1, 14, 1, 14, 14, 1, 1, 1, + 1, 1, 14, 1, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 14, 1, 14, 1, 1, 1, 14, 1, 14, 1, 1, 1, 1, 14, 1, 14, 1, + 14, 1, 14, 1, 1, 1, 1, 14, 1, 14, 1, 1, 1, 1, 1, 1, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 14, 14, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 19, 1, 1, 19, 1, 1, 19, 1, 19, 1, 1, 19, 18, 15, 1, 4, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 18, 16, 2, 6, 8, 3, 7, 13, 11, 19, 19, 19, 19, 19, 19, 13, 19, 19, 19, 19, 19, 19, 19, 18, 15, + 15, 15, 9, 1, 9, 10, 19, 19, 19, 1, 9, 10, 19, 19, 19, 1, 11, 14, 14, 14, 14, 14, 19, 1, 19, 1, 19, 1, 19, 1, 19, + 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 11, 1, 1, 1, 19, 1, 19, 1, 19, 19, 19, 1, 1, 19, 1, 1, 1, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 10, 11, 19, 19, 19, 19, 19, 19, 19, 19, 19, 1, 19, 19, + 19, 1, 19, 19, 19, 19, 19, 19, 19, 19, 9, 1, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 1, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 1, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 1, 1, 1, 1, 1, 19, 1, 14, 19, 19, 19, 1, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 14, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 18, 19, 19, 1, 1, 1, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 1, 14, 19, 1, 19, 1, 1, 1, 19, 19, 1, 14, 19, 1, 1, 19, 1, 19, 1, 1, 1, 1, + 1, 1, 1, 1, 19, 1, 1, 19, 1, 1, 19, 19, 1, 19, 1, 1, 1, 19, 1, 19, 1, 1, 19, 1, 19, 1, 19, 1, 19, 1, 1, 1, 1, 19, + 1, 1, 1, 1, 1, 19, 1, 1, 1, 1, 1, 14, 14, 19, 14, 19, 19, 1, 1, 1, 14, 1, 19, 19, 19, 1, 1, 1, 19, 1, 1, 1, 1, 1, + 1, 14, 1, 14, 1, 14, 1, 1, 14, 1, 19, 1, 1, 11, 11, 1, 19, 1, 1, 1, 14, 1, 1, 14, 1, 1, 1, 1, 1, 14, 1, 1, 14, 1, + 1, 1, 14, 1, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 14, 1, 14, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 4, 14, 4, 10, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 19, 19, 5, 5, 5, 5, 5, 15, 5, 5, 19, 5, 14, 19, + 19, 19, 19, 14, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 13, 19, 13, 19, 13, 19, 19, 19, 19, 19, 19, 19, 19, 11, 19, 10, 10, 19, 19, 11, 11, 19, 5, 5, 5, 5, 15, 19, 11, + 11, 11, 19, 19, 19, 19, 10, 13, 10, 13, 9, 13, 19, 19, 19, 1, 19, 19, 19, 19, 19, 19, 1, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 19, 19, 19, 11, 19, 19, 19, 15, 19, 19, 15, 1, 1, 1, 1, 1, 1, 1, 1, + 19, 1, 1, 1, 19, 19, 19, 19, 19, 1, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 19, 4, 4, 4, 4, 4, 14, 4, 14, 4, 14, 4, 4, 4, 4, 4, 4, 14, 4, 14, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 19, 4, 4, 4, 4, 4, 4, 4, 4, 4, 12, 4, 1, 14, 1, 1, 14, 1, 19, 1, 14, 1, 1, 1, 14, 1, 14, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 14, 1, 1, 15, 14, 1, 14, 1, 14, 1, 19, 14, 19, 19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 1, 1, 1, 19, 1, 1, 1, 19, 1, 1, 1, 19, 1, 1, 1, 19, 1, 9, 4, 19, 19, 19, 19, + 19, 19, 9, 1, 1, 1, 1, 1, 1, 1, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 1, 19, 19, 19, 1, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 15, 1, 1, 1, 1, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 14, 15, 15, 1, 15, 1, 15 + }; + + /** + * Lookup bidi class for character expressed as unicode scalar value. + * + * @param ch a unicode scalar value + * @return bidi class + */ + public static int getBidiClass(int ch) { + if (ch <= 0x00FF) { + return BC_L_1[ch - 0x0000]; + } else if ((ch >= 0x0590) && (ch <= 0x06FF)) { + return BC_R_1[ch - 0x0590]; + } else { + return getBidiClass(ch, BC_S_1, BC_E_1, BC_C_1); + } + } + + private static int getBidiClass(int ch, int[] sa, int[] ea, byte[] ca) { + int k = Arrays.binarySearch(sa, ch); + if (k >= 0) { + return ca[k]; + } else { + k = -(k + 1); + if (k == 0) { + return BidiConstants.L; + } else if (ch <= ea[k - 1]) { + return ca[k - 1]; + } else { + return BidiConstants.L; + } + } + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiConstants.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiConstants.java new file mode 100644 index 0000000..cfc7d72 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/bidi/BidiConstants.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.bidi; + +/** + *

Constants used for bidirectional processing.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface BidiConstants { + + /** + * first external (official) category + */ + int FIRST = 1; + + // strong category + + /** + * left-to-right class + */ + int L = 1; + + /** + * left-to-right embedding class + */ + int LRE = 2; + + /** + * left-to-right override class + */ + int LRO = 3; + + /** + * right-to-left class + */ + int R = 4; + + /** + * right-to-left arabic class + */ + int AL = 5; + + /** + * right-to-left embedding class + */ + int RLE = 6; + + /** + * right-to-left override class + */ + int RLO = 7; + + // weak category + + /** + * pop directional formatting class + */ + int PDF = 8; + + /** + * european number class + */ + int EN = 9; + + /** + * european number separator class + */ + int ES = 10; + + /** + * european number terminator class + */ + int ET = 11; + + /** + * arabic number class + */ + int AN = 12; + + /** + * common number separator class + */ + int CS = 13; + + /** + * non-spacing mark class + */ + int NSM = 14; + + /** + * boundary neutral class + */ + int BN = 15; + + // neutral category + + /** + * paragraph separator class + */ + int B = 16; + + /** + * segment separator class + */ + int S = 17; + + /** + * whitespace class + */ + int WS = 18; + + /** + * other neutrals class + */ + int ON = 19; + + /** + * last external (official) category + */ + int LAST = 19; + + // implementation specific categories + + /** + * placeholder for low surrogate + */ + int SURROGATE = 20; + + // other constants + + /** + * last + * /** maximum bidirectional levels + */ + int MAX_LEVELS = 61; + + /** + * override flag + */ + int OVERRIDE = 128; +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/AdvancedTypographicTableFormatException.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/AdvancedTypographicTableFormatException.java new file mode 100644 index 0000000..d790578 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/AdvancedTypographicTableFormatException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +/** + *

Exception thrown when attempting to decode a truetype font file and a format + * constraint is violated.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 Throwable that caused this exception + */ + public AdvancedTypographicTableFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassMapping.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassMapping.java new file mode 100644 index 0000000..1d59d22 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassMapping.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +/** + *

The GlyphClassMapping interface provides glyph identifier to class + * index mapping support.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface GlyphClassMapping { + + /** + * Obtain size of class table, i.e., ciMax + 1, where ciMax is the maximum + * class index. + * + * @param set for coverage set based class mappings, indicates set index, otherwise ignored + * @return size of class table + */ + int getClassSize(int set); + + /** + * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of + * the class table. + * + * @param gid glyph identifier (code) + * @param set for coverage set based class mappings, indicates set index, otherwise ignored + * @return non-negative glyph class index or -1 if glyph identifiers is not mapped by table + */ + int getClassIndex(int gid, int set); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassTable.java new file mode 100644 index 0000000..4d7ed9a --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphClassTable.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +import java.util.Iterator; +import java.util.List; + +/** + *

Base class implementation of glyph class table.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public final class GlyphClassTable extends GlyphMappingTable implements GlyphClassMapping { + + /** + * empty mapping table + */ + public static final int GLYPH_CLASS_TYPE_EMPTY = GLYPH_MAPPING_TYPE_EMPTY; + + /** + * mapped mapping table + */ + public static final int GLYPH_CLASS_TYPE_MAPPED = GLYPH_MAPPING_TYPE_MAPPED; + + /** + * range based mapping table + */ + public static final int GLYPH_CLASS_TYPE_RANGE = GLYPH_MAPPING_TYPE_RANGE; + + /** + * empty mapping table + */ + public static final int GLYPH_CLASS_TYPE_COVERAGE_SET = 3; + + private GlyphClassMapping cm; + + private GlyphClassTable(GlyphClassMapping cm) { + assert cm != null; + assert cm instanceof GlyphMappingTable; + this.cm = cm; + } + + /** + * {@inheritDoc} + */ + public int getType() { + return ((GlyphMappingTable) cm).getType(); + } + + /** + * {@inheritDoc} + */ + public List getEntries() { + return ((GlyphMappingTable) cm).getEntries(); + } + + /** + * {@inheritDoc} + */ + public int getClassSize(int set) { + return cm.getClassSize(set); + } + + /** + * {@inheritDoc} + */ + public int getClassIndex(int gid, int set) { + return cm.getClassIndex(gid, set); + } + + /** + * Create glyph class table. + * + * @param entries list of mapped or ranged class entries, or null or empty list + * @return a new covera table instance + */ + public static GlyphClassTable createClassTable(List entries) { + GlyphClassMapping cm; + if ((entries == null) || (entries.size() == 0)) { + cm = new EmptyClassTable(entries); + } else if (isMappedClass(entries)) { + cm = new MappedClassTable(entries); + } else if (isRangeClass(entries)) { + cm = new RangeClassTable(entries); + } else if (isCoverageSetClass(entries)) { + cm = new CoverageSetClassTable(entries); + } else { + cm = null; + } + assert cm != null : "unknown class type"; + return new GlyphClassTable(cm); + } + + private static boolean isMappedClass(List entries) { + if ((entries == null) || (entries.size() == 0)) { + return false; + } else { + for (Iterator it = entries.iterator(); it.hasNext(); ) { + Object o = it.next(); + if (!(o instanceof Integer)) { + return false; + } + } + return true; + } + } + + private static boolean isRangeClass(List entries) { + if ((entries == null) || (entries.size() == 0)) { + return false; + } else { + for (Iterator it = entries.iterator(); it.hasNext(); ) { + Object o = it.next(); + if (!(o instanceof MappingRange)) { + return false; + } + } + return true; + } + } + + private static boolean isCoverageSetClass(List entries) { + if ((entries == null) || (entries.size() == 0)) { + return false; + } else { + for (Iterator it = entries.iterator(); it.hasNext(); ) { + Object o = it.next(); + if (!(o instanceof GlyphCoverageTable)) { + return false; + } + } + return true; + } + } + + private static class EmptyClassTable extends GlyphMappingTable.EmptyMappingTable implements GlyphClassMapping { + + public EmptyClassTable(List entries) { + super(entries); + } + + /** + * {@inheritDoc} + */ + public int getClassSize(int set) { + return 0; + } + + /** + * {@inheritDoc} + */ + public int getClassIndex(int gid, int set) { + return -1; + } + } + + private static class MappedClassTable extends GlyphMappingTable.MappedMappingTable implements GlyphClassMapping { + + private int firstGlyph; + private int[] gca; + private int gcMax = -1; + + public MappedClassTable(List entries) { + populate(entries); + } + + /** + * {@inheritDoc} + */ + public List getEntries() { + List entries = new java.util.ArrayList(); + entries.add(Integer.valueOf(firstGlyph)); + if (gca != null) { + for (int i = 0, n = gca.length; i < n; i++) { + entries.add(Integer.valueOf(gca[i])); + } + } + return entries; + } + + /** + * {@inheritDoc} + */ + public int getMappingSize() { + return gcMax + 1; + } + + /** + * {@inheritDoc} + */ + public int getMappedIndex(int gid) { + int i = gid - firstGlyph; + if ((i >= 0) && (i < gca.length)) { + return gca[i]; + } else { + return -1; + } + } + + /** + * {@inheritDoc} + */ + public int getClassSize(int set) { + return getMappingSize(); + } + + /** + * {@inheritDoc} + */ + public int getClassIndex(int gid, int set) { + return getMappedIndex(gid); + } + + private void populate(List entries) { + // obtain entries iterator + Iterator it = entries.iterator(); + // extract first glyph + int firstGlyph = 0; + if (it.hasNext()) { + Object o = it.next(); + if (o instanceof Integer) { + firstGlyph = ((Integer) o).intValue(); + } else { + throw new AdvancedTypographicTableFormatException( + "illegal entry, first entry must be Integer denoting first glyph value, but is: " + o); + } + } + // extract glyph class array + int i = 0; + int n = entries.size() - 1; + int gcMax = -1; + int[] gca = new int[n]; + while (it.hasNext()) { + Object o = it.next(); + if (o instanceof Integer) { + int gc = ((Integer) o).intValue(); + gca[i++] = gc; + if (gc > gcMax) { + gcMax = gc; + } + } else { + throw new AdvancedTypographicTableFormatException("illegal mapping entry, must be Integer: " + o); + } + } + assert i == n; + assert this.gca == null; + this.firstGlyph = firstGlyph; + this.gca = gca; + this.gcMax = gcMax; + } + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("{ firstGlyph = " + firstGlyph + ", classes = {"); + for (int i = 0, n = gca.length; i < n; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(Integer.toString(gca[i])); + } + sb.append("} }"); + return sb.toString(); + } + } + + private static class RangeClassTable extends GlyphMappingTable.RangeMappingTable implements GlyphClassMapping { + + public RangeClassTable(List entries) { + super(entries); + } + + /** + * {@inheritDoc} + */ + public int getMappedIndex(int gid, int s, int m) { + return m; + } + + /** + * {@inheritDoc} + */ + public int getClassSize(int set) { + return getMappingSize(); + } + + /** + * {@inheritDoc} + */ + public int getClassIndex(int gid, int set) { + return getMappedIndex(gid); + } + } + + private static class CoverageSetClassTable extends GlyphMappingTable.EmptyMappingTable implements GlyphClassMapping { + + public CoverageSetClassTable(List entries) { + throw new UnsupportedOperationException("coverage set class table not yet supported"); + } + + /** + * {@inheritDoc} + */ + public int getType() { + return GLYPH_CLASS_TYPE_COVERAGE_SET; + } + + /** + * {@inheritDoc} + */ + public int getClassSize(int set) { + return 0; + } + + /** + * {@inheritDoc} + */ + public int getClassIndex(int gid, int set) { + return -1; + } + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphContextTester.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphContextTester.java new file mode 100644 index 0000000..3ae3833 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphContextTester.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +import com.jaredrummler.fontreader.util.GlyphSequence; + +/** + *

Interface for testing the originating (source) character context of a glyph sequence.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface GlyphContextTester { + + /** + * Perform a test on a glyph sequence in a specific (originating) character context. + * + * @param script governing script + * @param language governing language + * @param feature governing feature + * @param gs glyph sequence to test + * @param index index into glyph sequence to test + * @param flags that apply to lookup in scope + * @return true if test is satisfied + */ + boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageMapping.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageMapping.java new file mode 100644 index 0000000..2a28895 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageMapping.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +/** + *

The GlyphCoverageMapping interface provides glyph identifier to coverage + * index mapping support.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface GlyphCoverageMapping { + + /** + * Obtain size of coverage table, i.e., ciMax + 1, where ciMax is the maximum + * coverage index. + * + * @return size of coverage table + */ + int getCoverageSize(); + + /** + * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of + * the coverage table. + * + * @param gid glyph identifier (code) + * @return non-negative glyph coverage index or -1 if glyph identifiers is not mapped by table + */ + int getCoverageIndex(int gid); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageTable.java new file mode 100644 index 0000000..eb682f2 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphCoverageTable.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + *

.Base class implementation of glyph coverage table.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public final class GlyphCoverageTable extends GlyphMappingTable implements GlyphCoverageMapping { + + /** + * empty mapping table + */ + public static final int GLYPH_COVERAGE_TYPE_EMPTY = GLYPH_MAPPING_TYPE_EMPTY; + + /** + * mapped mapping table + */ + public static final int GLYPH_COVERAGE_TYPE_MAPPED = GLYPH_MAPPING_TYPE_MAPPED; + + /** + * range based mapping table + */ + public static final int GLYPH_COVERAGE_TYPE_RANGE = GLYPH_MAPPING_TYPE_RANGE; + + private GlyphCoverageMapping cm; + + private GlyphCoverageTable(GlyphCoverageMapping cm) { + this.cm = cm; + } + + /** + * {@inheritDoc} + */ + public int getType() { + return ((GlyphMappingTable) cm).getType(); + } + + /** + * {@inheritDoc} + */ + public List getEntries() { + return ((GlyphMappingTable) cm).getEntries(); + } + + /** + * {@inheritDoc} + */ + public int getCoverageSize() { + return cm.getCoverageSize(); + } + + /** + * {@inheritDoc} + */ + public int getCoverageIndex(int gid) { + return cm.getCoverageIndex(gid); + } + + /** + * Create glyph coverage table. + * + * @param entries list of mapped or ranged coverage entries, or null or empty list + * @return a new covera table instance + */ + public static GlyphCoverageTable createCoverageTable(List entries) { + GlyphCoverageMapping cm; + if ((entries == null) || (entries.size() == 0)) { + cm = new EmptyCoverageTable(entries); + } else if (isMappedCoverage(entries)) { + cm = new MappedCoverageTable(entries); + } else if (isRangeCoverage(entries)) { + cm = new RangeCoverageTable(entries); + } else { + cm = null; + } + assert cm != null : "unknown coverage type"; + return new GlyphCoverageTable(cm); + } + + private static boolean isMappedCoverage(List entries) { + if ((entries == null) || (entries.size() == 0)) { + return false; + } else { + for (Iterator it = entries.iterator(); it.hasNext(); ) { + Object o = it.next(); + if (!(o instanceof Integer)) { + return false; + } + } + return true; + } + } + + private static boolean isRangeCoverage(List entries) { + if ((entries == null) || (entries.size() == 0)) { + return false; + } else { + for (Iterator it = entries.iterator(); it.hasNext(); ) { + Object o = it.next(); + if (!(o instanceof MappingRange)) { + return false; + } + } + return true; + } + } + + private static class EmptyCoverageTable extends GlyphMappingTable.EmptyMappingTable implements GlyphCoverageMapping { + + public EmptyCoverageTable(List entries) { + super(entries); + } + + /** + * {@inheritDoc} + */ + public int getCoverageSize() { + return 0; + } + + /** + * {@inheritDoc} + */ + public int getCoverageIndex(int gid) { + return -1; + } + } + + private static class MappedCoverageTable extends GlyphMappingTable.MappedMappingTable + implements GlyphCoverageMapping { + + private int[] map; + + public MappedCoverageTable(List entries) { + populate(entries); + } + + /** + * {@inheritDoc} + */ + public List getEntries() { + List entries = new java.util.ArrayList(); + if (map != null) { + for (int i = 0, n = map.length; i < n; i++) { + entries.add(Integer.valueOf(map[i])); + } + } + return entries; + } + + /** + * {@inheritDoc} + */ + public int getMappingSize() { + return (map != null) ? map.length : 0; + } + + public int getMappedIndex(int gid) { + int i; + if ((i = Arrays.binarySearch(map, gid)) >= 0) { + return i; + } else { + return -1; + } + } + + /** + * {@inheritDoc} + */ + public int getCoverageSize() { + return getMappingSize(); + } + + /** + * {@inheritDoc} + */ + public int getCoverageIndex(int gid) { + return getMappedIndex(gid); + } + + private void populate(List entries) { + int i = 0; + int skipped = 0; + int n = entries.size(); + int gidMax = -1; + int[] map = new int[n]; + for (Iterator it = entries.iterator(); it.hasNext(); ) { + Object o = it.next(); + if (o instanceof Integer) { + int gid = ((Integer) o).intValue(); + if ((gid >= 0) && (gid < 65536)) { + if (gid > gidMax) { + map[i++] = gidMax = gid; + } else { + skipped++; + } + } else { + throw new AdvancedTypographicTableFormatException("illegal glyph index: " + gid); + } + } else { + throw new AdvancedTypographicTableFormatException("illegal coverage entry, must be Integer: " + o); + } + } + assert (i + skipped) == n; + assert this.map == null; + this.map = map; + } + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append('{'); + for (int i = 0, n = map.length; i < n; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(Integer.toString(map[i])); + } + sb.append('}'); + return sb.toString(); + } + } + + private static class RangeCoverageTable extends GlyphMappingTable.RangeMappingTable implements GlyphCoverageMapping { + + public RangeCoverageTable(List entries) { + super(entries); + } + + /** + * {@inheritDoc} + */ + public int getMappedIndex(int gid, int s, int m) { + return m + gid - s; + } + + /** + * {@inheritDoc} + */ + public int getCoverageSize() { + return getMappingSize(); + } + + /** + * {@inheritDoc} + */ + public int getCoverageIndex(int gid) { + return getMappedIndex(gid); + } + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinition.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinition.java new file mode 100644 index 0000000..f7c510f --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinition.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +/** + *

The GlyphDefinition interface is a marker interface implemented by a glyph definition + * subtable.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface GlyphDefinition { + + /** + * Determine if some definition is available for a specific glyph. + * + * @param gi a glyph index + * @return true if some (unspecified) definition is available for the specified glyph + */ + boolean hasDefinition(int gi); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionSubtable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionSubtable.java new file mode 100644 index 0000000..aaf5fc7 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionSubtable.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +import com.jaredrummler.fontreader.fonts.GlyphSubtable; +import com.jaredrummler.fontreader.truetype.GlyphTable; + +/** + *

The GlyphDefinitionSubtable implements an abstract base of a glyph definition subtable, + * providing a default implementation of the GlyphDefinition interface.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public abstract class GlyphDefinitionSubtable extends GlyphSubtable implements GlyphDefinition { + + /** + * Instantiate a GlyphDefinitionSubtable. + * + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param mapping subtable coverage table + */ + protected GlyphDefinitionSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping) { + super(id, sequence, flags, format, mapping); + } + + /** + * {@inheritDoc} + */ + public int getTableType() { + return GlyphTable.GLYPH_TABLE_TYPE_DEFINITION; + } + + /** + * {@inheritDoc} + */ + public String getTypeName() { + return GlyphDefinitionTable.getLookupTypeName(getType()); + } + + /** + * {@inheritDoc} + */ + public boolean usesReverseScan() { + return false; + } + + /** + * {@inheritDoc} + */ + public boolean hasDefinition(int gi) { + GlyphCoverageMapping cvm; + if ((cvm = getCoverage()) != null) { + if (cvm.getCoverageIndex(gi) >= 0) { + return true; + } + } + GlyphClassMapping clm; + if ((clm = getClasses()) != null) { + if (clm.getClassIndex(gi, 0) >= 0) { + return true; + } + } + return false; + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionTable.java new file mode 100644 index 0000000..5c8b6a9 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphDefinitionTable.java @@ -0,0 +1,548 @@ +/* + * Copyright (C) 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. + * + */ + +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; + +/** + *

The GlyphDefinitionTable class is a glyph table that implements + * glyph definition functionality according to the OpenType GDEF table.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 GlyphDefinitionTable object using the specified subtables. + * + * @param subtables a list of identified subtables + */ + public GlyphDefinitionTable(List subtables) { + super(null, new HashMap(0)); + if ((subtables == null) || (subtables.size() == 0)) { + throw new AdvancedTypographicTableFormatException("subtables must be non-empty"); + } else { + for (Iterator it = subtables.iterator(); it.hasNext(); ) { + Object o = it.next(); + if (o instanceof GlyphDefinitionSubtable) { + addSubtable((GlyphSubtable) o); + } else { + throw new AdvancedTypographicTableFormatException("subtable must be a glyph definition subtable"); + } + } + freezeSubtables(); + } + } + + /** + * Reorder combining marks in glyph sequence so that they precede (within the sequence) the base + * character to which they are applied. N.B. In the case of LTR segments, marks are not reordered by this, + * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede + * their base glyph. + * + * @param gs an input glyph sequence + * @param widths associated advance widths (also reordered) + * @param gpa associated glyph position adjustments (also reordered) + * @param script a script identifier + * @param language a language identifier + * @return the reordered (output) glyph sequence + */ + public GlyphSequence reorderCombiningMarks(GlyphSequence gs, int[] widths, int[][] gpa, String script, + String language) { + ScriptProcessor sp = ScriptProcessor.getInstance(script); + return sp.reorderCombiningMarks(this, gs, widths, gpa, script, language); + } + + /** + * {@inheritDoc} + */ + protected void addSubtable(GlyphSubtable subtable) { + if (subtable instanceof GlyphClassSubtable) { + this.gct = (GlyphClassSubtable) subtable; + } else if (subtable instanceof AttachmentPointSubtable) { + // TODO - not yet used + // this.apt = (AttachmentPointSubtable) subtable; + } else if (subtable instanceof LigatureCaretSubtable) { + // TODO - not yet used + // this.lct = (LigatureCaretSubtable) subtable; + } else if (subtable instanceof MarkAttachmentSubtable) { + this.mat = (MarkAttachmentSubtable) subtable; + } else { + throw new UnsupportedOperationException("unsupported glyph definition subtable type: " + subtable); + } + } + + /** + * Determine if glyph belongs to pre-defined glyph class. + * + * @param gid a glyph identifier (index) + * @param gc a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + * @return true if glyph belongs to specified glyph class + */ + public boolean isGlyphClass(int gid, int gc) { + if (gct != null) { + return gct.isGlyphClass(gid, gc); + } else { + return false; + } + } + + /** + * Determine glyph class. + * + * @param gid a glyph identifier (index) + * @return a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + */ + public int getGlyphClass(int gid) { + if (gct != null) { + return gct.getGlyphClass(gid); + } else { + return -1; + } + } + + /** + * Determine if glyph belongs to (font specific) mark attachment class. + * + * @param gid a glyph identifier (index) + * @param mac a (font specific) mark attachment class + * @return true if glyph belongs to specified mark attachment class + */ + public boolean isMarkAttachClass(int gid, int mac) { + if (mat != null) { + return mat.isMarkAttachClass(gid, mac); + } else { + return false; + } + } + + /** + * Determine mark attachment class. + * + * @param gid a glyph identifier (index) + * @return a non-negative mark attachment class, or -1 if no class defined + */ + public int getMarkAttachClass(int gid) { + if (mat != null) { + return mat.getMarkAttachClass(gid); + } else { + return -1; + } + } + + /** + * Map a lookup type name to its constant (integer) value. + * + * @param name lookup type name + * @return lookup type + */ + public static int getLookupTypeFromName(String name) { + int t; + String s = name.toLowerCase(); + if ("glyphclass".equals(s)) { + t = GDEF_LOOKUP_TYPE_GLYPH_CLASS; + } else if ("attachmentpoint".equals(s)) { + t = GDEF_LOOKUP_TYPE_ATTACHMENT_POINT; + } else if ("ligaturecaret".equals(s)) { + t = GDEF_LOOKUP_TYPE_LIGATURE_CARET; + } else if ("markattachment".equals(s)) { + t = GDEF_LOOKUP_TYPE_MARK_ATTACHMENT; + } else { + t = -1; + } + return t; + } + + /** + * Map a lookup type constant (integer) value to its name. + * + * @param type lookup type + * @return lookup type name + */ + public static String getLookupTypeName(int type) { + String tn = null; + switch (type) { + case GDEF_LOOKUP_TYPE_GLYPH_CLASS: + tn = "glyphclass"; + break; + case GDEF_LOOKUP_TYPE_ATTACHMENT_POINT: + tn = "attachmentpoint"; + break; + case GDEF_LOOKUP_TYPE_LIGATURE_CARET: + tn = "ligaturecaret"; + break; + case GDEF_LOOKUP_TYPE_MARK_ATTACHMENT: + tn = "markattachment"; + break; + default: + tn = "unknown"; + break; + } + return tn; + } + + /** + * Create a definition subtable according to the specified arguments. + * + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags (must be zero) + * @param format subtable format + * @param mapping subtable mapping table + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format, + GlyphMappingTable mapping, List entries) { + GlyphSubtable st = null; + switch (type) { + case GDEF_LOOKUP_TYPE_GLYPH_CLASS: + st = GlyphClassSubtable.create(id, sequence, flags, format, mapping, entries); + break; + case GDEF_LOOKUP_TYPE_ATTACHMENT_POINT: + st = AttachmentPointSubtable.create(id, sequence, flags, format, mapping, entries); + break; + case GDEF_LOOKUP_TYPE_LIGATURE_CARET: + st = LigatureCaretSubtable.create(id, sequence, flags, format, mapping, entries); + break; + case GDEF_LOOKUP_TYPE_MARK_ATTACHMENT: + st = MarkAttachmentSubtable.create(id, sequence, flags, format, mapping, entries); + break; + default: + break; + } + return st; + } + + private abstract static class GlyphClassSubtable extends GlyphDefinitionSubtable { + + GlyphClassSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping); + } + + /** + * {@inheritDoc} + */ + public int getType() { + return GDEF_LOOKUP_TYPE_GLYPH_CLASS; + } + + /** + * Determine if glyph belongs to pre-defined glyph class. + * + * @param gid a glyph identifier (index) + * @param gc a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + * @return true if glyph belongs to specified glyph class + */ + public abstract boolean isGlyphClass(int gid, int gc); + + /** + * Determine glyph class. + * + * @param gid a glyph identifier (index) + * @return a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + */ + public abstract int getGlyphClass(int gid); + + static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping, + List entries) { + if (format == 1) { + return new GlyphClassSubtableFormat1(id, sequence, flags, format, mapping, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class GlyphClassSubtableFormat1 extends GlyphClassSubtable { + + GlyphClassSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping, entries); + } + + /** + * {@inheritDoc} + */ + public List getEntries() { + return null; + } + + /** + * {@inheritDoc} + */ + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof GlyphClassSubtable; + } + + /** + * {@inheritDoc} + */ + public boolean isGlyphClass(int gid, int gc) { + GlyphClassMapping cm = getClasses(); + if (cm != null) { + return cm.getClassIndex(gid, 0) == gc; + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + public int getGlyphClass(int gid) { + GlyphClassMapping cm = getClasses(); + if (cm != null) { + return cm.getClassIndex(gid, 0); + } else { + return -1; + } + } + } + + private abstract static class AttachmentPointSubtable extends GlyphDefinitionSubtable { + + AttachmentPointSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping); + } + + /** + * {@inheritDoc} + */ + public int getType() { + return GDEF_LOOKUP_TYPE_ATTACHMENT_POINT; + } + + static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping, + List entries) { + if (format == 1) { + return new AttachmentPointSubtableFormat1(id, sequence, flags, format, mapping, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class AttachmentPointSubtableFormat1 extends AttachmentPointSubtable { + + AttachmentPointSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping, + List entries) { + super(id, sequence, flags, format, mapping, entries); + } + + /** + * {@inheritDoc} + */ + public List getEntries() { + return null; + } + + /** + * {@inheritDoc} + */ + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof AttachmentPointSubtable; + } + } + + private abstract static class LigatureCaretSubtable extends GlyphDefinitionSubtable { + + LigatureCaretSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping); + } + + /** + * {@inheritDoc} + */ + public int getType() { + return GDEF_LOOKUP_TYPE_LIGATURE_CARET; + } + + static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping, + List entries) { + if (format == 1) { + return new LigatureCaretSubtableFormat1(id, sequence, flags, format, mapping, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class LigatureCaretSubtableFormat1 extends LigatureCaretSubtable { + + LigatureCaretSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping, + List entries) { + super(id, sequence, flags, format, mapping, entries); + } + + /** + * {@inheritDoc} + */ + public List getEntries() { + return null; + } + + /** + * {@inheritDoc} + */ + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof LigatureCaretSubtable; + } + } + + private abstract static class MarkAttachmentSubtable extends GlyphDefinitionSubtable { + + MarkAttachmentSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping); + } + + /** + * {@inheritDoc} + */ + public int getType() { + return GDEF_LOOKUP_TYPE_MARK_ATTACHMENT; + } + + /** + * Determine if glyph belongs to (font specific) mark attachment class. + * + * @param gid a glyph identifier (index) + * @param mac a (font specific) mark attachment class + * @return true if glyph belongs to specified mark attachment class + */ + public abstract boolean isMarkAttachClass(int gid, int mac); + + /** + * Determine mark attachment class. + * + * @param gid a glyph identifier (index) + * @return a non-negative mark attachment class, or -1 if no class defined + */ + public abstract int getMarkAttachClass(int gid); + + static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping, + List entries) { + if (format == 1) { + return new MarkAttachmentSubtableFormat1(id, sequence, flags, format, mapping, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MarkAttachmentSubtableFormat1 extends MarkAttachmentSubtable { + + MarkAttachmentSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping, + List entries) { + super(id, sequence, flags, format, mapping, entries); + } + + /** + * {@inheritDoc} + */ + public List getEntries() { + return null; + } + + /** + * {@inheritDoc} + */ + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof MarkAttachmentSubtable; + } + + /** + * {@inheritDoc} + */ + public boolean isMarkAttachClass(int gid, int mac) { + GlyphClassMapping cm = getClasses(); + if (cm != null) { + return cm.getClassIndex(gid, 0) == mac; + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + public int getMarkAttachClass(int gid) { + GlyphClassMapping cm = getClasses(); + if (cm != null) { + return cm.getClassIndex(gid, 0); + } else { + return -1; + } + } + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphMappingTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphMappingTable.java new file mode 100644 index 0000000..8bb9d6c --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphMappingTable.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + *

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.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 MappingRange class encapsulates a glyph [start,end] range and + * a mapping index. + */ + public static class MappingRange { + + private final int gidStart; // first glyph in range (inclusive) + private final int gidEnd; // last glyph in range (inclusive) + private final int index; // mapping index; + + /** + * Instantiate a mapping range. + */ + public MappingRange() { + this(0, 0, 0); + } + + /** + * Instantiate a specific mapping range. + * + * @param gidStart start of range + * @param gidEnd end of range + * @param index mapping index + */ + public MappingRange(int gidStart, int gidEnd, int index) { + if ((gidStart < 0) || (gidEnd < 0) || (index < 0)) { + throw new AdvancedTypographicTableFormatException(); + } else if (gidStart > gidEnd) { + throw new AdvancedTypographicTableFormatException(); + } else { + this.gidStart = gidStart; + this.gidEnd = gidEnd; + this.index = index; + } + } + + /** + * @return start of range + */ + public int getStart() { + return gidStart; + } + + /** + * @return end of range + */ + public int getEnd() { + return gidEnd; + } + + /** + * @return mapping index + */ + public int getIndex() { + return index; + } + + /** + * @return interval as a pair of integers + */ + public int[] getInterval() { + return new int[]{gidStart, gidEnd}; + } + + /** + * Obtain interval, filled into first two elements of specified array, or returning new array. + * + * @param interval an array of length two or greater or null + * @return interval as a pair of integers, filled into specified array + */ + public int[] getInterval(int[] interval) { + if ((interval == null) || (interval.length != 2)) { + throw new IllegalArgumentException(); + } else { + interval[0] = gidStart; + interval[1] = gidEnd; + } + return interval; + } + + /** + * @return length of interval + */ + public int getLength() { + return gidStart - gidEnd; + } + + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioning.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioning.java new file mode 100644 index 0000000..feaab2f --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioning.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +/** + *

The GlyphPositioning 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.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface GlyphPositioning { + + /** + * Perform glyph positioning at the current index, mutating the positioning state object as required. + * Only the context associated with the current index is processed. + * + * @param ps glyph positioning state object + * @return true if the glyph subtable applies, meaning that the current context matches the + * associated input context glyph coverage table; note that returning true does not mean any position + * adjustment occurred; it only means that no further glyph subtables for the current lookup table + * should be applied. + */ + boolean position(GlyphPositioningState ps); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningState.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningState.java new file mode 100644 index 0000000..0bdb7f9 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningState.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +import com.jaredrummler.fontreader.truetype.GlyphTable; +import com.jaredrummler.fontreader.util.GlyphSequence; +import com.jaredrummler.fontreader.util.ScriptContextTester; + +/** + *

The GlyphPositioningState implements an state object used during glyph positioning + * processing.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 nig input glyphs + * starting at the current position. If lookups are non-null and non-empty, then + * all input glyphs specified by nig are consumed irregardless of + * whether any specified lookup applied. + * + * @param lookups array of matched lookups (or null) + * @param nig number of glyphs in input sequence, starting at current position, to which + * the lookups are to apply, and to be consumed once the application has finished + * @return true if lookups are non-null and non-empty; otherwise, false + */ + public boolean apply(GlyphTable.RuleLookup[] lookups, int nig) { + if ((lookups != null) && (lookups.length > 0)) { + // apply each rule lookup to extracted input glyph array + for (int i = 0, n = lookups.length; i < n; i++) { + GlyphTable.RuleLookup l = lookups[i]; + if (l != null) { + GlyphTable.LookupTable lt = l.getLookup(); + if (lt != null) { + // perform positioning on a copy of previous state + GlyphPositioningState ps = new GlyphPositioningState(this); + // apply lookup table positioning + if (lt.position(ps, l.getSequenceIndex())) { + setAdjusted(true); + } + } + } + } + consume(nig); + return true; + } else { + return false; + } + } + + /** + * Apply default application semantices; namely, consume one input glyph. + */ + public void applyDefault() { + super.applyDefault(); + } + + /** + * Set adjusted state, used to record effect of non-zero adjustment. + * + * @param adjusted true if to set adjusted state, otherwise false to + * clear adjusted state + */ + public void setAdjusted(boolean adjusted) { + this.adjusted = adjusted; + } + + /** + * Get adjusted state. + * + * @return adjusted true if some non-zero adjustment occurred and + * was recorded by {@link #setAdjusted}; otherwise, false. + */ + public boolean getAdjusted() { + return adjusted; + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningSubtable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningSubtable.java new file mode 100644 index 0000000..8afe96b --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningSubtable.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 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. + * + */ + +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; + +/** + *

The GlyphPositioningSubtable implements an abstract base of a glyph subtable, + * providing a default implementation of the GlyphPositioning interface.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public abstract class GlyphPositioningSubtable extends GlyphSubtable implements GlyphPositioning { + + /** + * Instantiate a GlyphPositioningSubtable. + * + * @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 sequenceIndex is non-negative, then apply subtables only when current position + * matches sequenceIndex in relation to the starting position. Furthermore, upon + * successful application at sequenceIndex, then discontinue processing the remaining + * + * @param ps positioning state + * @param sta array of subtables to apply + * @param sequenceIndex if non negative, then apply subtables only at specified sequence index + * @return true if a non-zero adjustment occurred + */ + public static boolean position(GlyphPositioningState ps, GlyphPositioningSubtable[] sta, int sequenceIndex) { + int sequenceStart = ps.getPosition(); + boolean appliedOneShot = false; + while (ps.hasNext()) { + boolean applied = false; + if (!appliedOneShot && ps.maybeApplicable()) { + for (int i = 0, n = sta.length; !applied && (i < n); i++) { + if (sequenceIndex < 0) { + applied = ps.apply(sta[i]); + } else if (ps.getPosition() == (sequenceStart + sequenceIndex)) { + applied = ps.apply(sta[i]); + if (applied) { + appliedOneShot = true; + } + } + } + } + if (!applied || !ps.didConsume()) { + ps.applyDefault(); + } + ps.next(); + } + return ps.getAdjusted(); + } + + /** + * Apply positioning. + * + * @param gs input glyph sequence + * @param script tag + * @param language tag + * @param feature tag + * @param fontSize the font size + * @param sta subtable array + * @param widths array + * @param adjustments array (receives output adjustments) + * @param sct script context tester + * @return true if a non-zero adjustment occurred + */ + public static boolean position(GlyphSequence gs, String script, String language, String feature, int fontSize, + GlyphPositioningSubtable[] sta, int[] widths, int[][] adjustments, + ScriptContextTester sct) { + GlyphPositioningState state = new GlyphPositioningState(); + return position(state.reset(gs, script, language, feature, fontSize, widths, adjustments, sct), sta, -1); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningTable.java new file mode 100644 index 0000000..073fe30 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphPositioningTable.java @@ -0,0 +1,2754 @@ +/* + * Copyright (C) 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. + * + */ + +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.*; + +/** + *

The GlyphPositioningTable class is a glyph table that implements + * GlyphPositioning functionality.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 GlyphPositioningTable 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/*>*/ 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 ci1 and entry anchor for second + * glyph with coverage index ci2. + * + * @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/**/ 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/**/ 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/**/ 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/**/ 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/**/ 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/**/ 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 DeviceTable 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 Value 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 PairValues 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 Anchor 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 MarkAnchor class is a subclass of the Anchor class, adding a mark + * class designation. + */ + public static class MarkAnchor extends Anchor { + + private final int markClass; // mark class + + /** + * Instantiate a MarkAnchor + * + * @param markClass the mark class + * @param a the underlying anchor (whose fields are copied) + */ + public MarkAnchor(int markClass, Anchor a) { + super(a); + this.markClass = markClass; + } + + /** + * @return the mark class + */ + public int getMarkClass() { + return markClass; + } + + /** + * {@inheritDoc} + */ + public String toString() { + return "{ markClass = " + markClass + ", anchor = " + super.toString() + " }"; + } + + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphProcessingState.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphProcessingState.java new file mode 100644 index 0000000..6ad495f --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/GlyphProcessingState.java @@ -0,0 +1,1359 @@ +/* + * Copyright (C) 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. + * + */ + +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; + +/** + *

The GlyphProcessingState implements a common, base state object used during glyph substitution + * and positioning processing.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 count glyphs remain in + * input sequence. + * + * @param count of glyphs to test + * @return true if at least count 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 count backtrack (previous) glyphs + * are present in input sequence. + * + * @param count of glyphs to test + * @return true if at least count 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 count 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 count glyphs starting at specified offset from current position. If + * reverseOrder 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 count 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 count ignored glyphs starting at specified offset from current position. If + * reverseOrder 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 count ignored glyphs starting at specified offset from current position. If + * offset + * 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 offset 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 offset 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 + * reverseOrder 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 + * reverseOrder 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 count character associations of glyphs starting at specified offset from current position. If + * reverseOrder 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 count 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 count character associations of ignored glyphs starting at specified offset from current + * position. If + * reverseOrder 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 count 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 count glyphs with a subsequence of the sequence gs starting from the specified + * offset gsOffset of length gsCount 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 gsOffset + * @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 count glyphs with all glyphs in the replacement sequence gs. + * + * @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 glyphs array is matched, one at a time, and when a (forward searching) match is + * found + * in the input glyph sequence, the matching glyph is replaced with the glyph index 65535. + * + * @param offset from current position + * @param glyphs array of glyphs to erase + * @return the number of glyphs erased, which may be less than the number of specified glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int erase(int offset, int[] glyphs) throws IndexOutOfBoundsException { + int start = index + offset; + if ((start < 0) || (start > indexLast)) { + throw new IndexOutOfBoundsException("will attempt index at " + start); + } else { + int erased = 0; + for (int i = start - index, n = indexLast - start; i < n; i++) { + int gi = getGlyph(i); + if (gi == glyphs[erased]) { + setGlyph(i, 65535); + erased++; + } + } + return erased; + } + } + + /** + * Determine if is possible that the current input sequence satisfies a script specific + * context testing predicate. If no predicate applies, then application is always possible. + * + * @return true if no script specific context tester applies or if a specified tester returns + * true for the current input sequence context + */ + public boolean maybeApplicable() { + if (gct == null) { + return true; + } else { + return gct.test(script, language, feature, igs, index, getLookupFlags()); + } + } + + /** + * Apply default application semantices; namely, consume one glyph. + */ + public void applyDefault() { + consumed += 1; + } + + /** + * Determine if specified glyph is a base glyph according to the governing + * glyph definition table. + * + * @param gi glyph index to test + * @return true if glyph definition table records glyph as a base glyph; otherwise, false + */ + public boolean isBase(int gi) { + if (gdef != null) { + return gdef.isGlyphClass(gi, GlyphDefinitionTable.GLYPH_CLASS_BASE); + } else { + return false; + } + } + + /** + * Determine if specified glyph is an ignored base glyph according to the governing + * glyph definition table. + * + * @param gi glyph index to test + * @param flags that apply to lookup in scope + * @return true if glyph definition table records glyph as a base glyph; otherwise, false + */ + public boolean isIgnoredBase(int gi, int flags) { + return ((flags & GlyphSubtable.LF_IGNORE_BASE) != 0) && isBase(gi); + } + + /** + * Determine if specified glyph is an ligature glyph according to the governing + * glyph definition table. + * + * @param gi glyph index to test + * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false + */ + public boolean isLigature(int gi) { + if (gdef != null) { + return gdef.isGlyphClass(gi, GlyphDefinitionTable.GLYPH_CLASS_LIGATURE); + } else { + return false; + } + } + + /** + * Determine if specified glyph is an ignored ligature glyph according to the governing + * glyph definition table. + * + * @param gi glyph index to test + * @param flags that apply to lookup in scope + * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false + */ + public boolean isIgnoredLigature(int gi, int flags) { + return ((flags & GlyphSubtable.LF_IGNORE_LIGATURE) != 0) && isLigature(gi); + } + + /** + * Determine if specified glyph is a mark glyph according to the governing + * glyph definition table. + * + * @param gi glyph index to test + * @return true if glyph definition table records glyph as a mark glyph; otherwise, false + */ + public boolean isMark(int gi) { + if (gdef != null) { + return gdef.isGlyphClass(gi, GlyphDefinitionTable.GLYPH_CLASS_MARK); + } else { + return false; + } + } + + /** + * Determine if specified glyph is an ignored ligature glyph according to the governing + * glyph definition table. + * + * @param gi glyph index to test + * @param flags that apply to lookup in scope + * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false + */ + public boolean isIgnoredMark(int gi, int flags) { + if ((flags & GlyphSubtable.LF_IGNORE_MARK) != 0) { + return isMark(gi); + } else if ((flags & GlyphSubtable.LF_MARK_ATTACHMENT_TYPE) != 0) { + int lac = (flags & GlyphSubtable.LF_MARK_ATTACHMENT_TYPE) >> 8; + int gac = gdef.getMarkAttachClass(gi); + return (gac != lac); + } else { + return false; + } + } + + /** + * Obtain an ignored glyph tester that corresponds to the specified lookup flags. + * + * @param flags lookup flags + * @return a glyph tester + */ + public GlyphTester getIgnoreTester(int flags) { + if ((flags & GlyphSubtable.LF_IGNORE_BASE) != 0) { + if ((flags & (GlyphSubtable.LF_IGNORE_LIGATURE | GlyphSubtable.LF_IGNORE_MARK)) == 0) { + return ignoreBase; + } else { + return getCombinedIgnoreTester(flags); + } + } + if ((flags & GlyphSubtable.LF_IGNORE_LIGATURE) != 0) { + if ((flags & (GlyphSubtable.LF_IGNORE_BASE | GlyphSubtable.LF_IGNORE_MARK)) == 0) { + return ignoreLigature; + } else { + return getCombinedIgnoreTester(flags); + } + } + if ((flags & GlyphSubtable.LF_IGNORE_MARK) != 0) { + if ((flags & (GlyphSubtable.LF_IGNORE_BASE | GlyphSubtable.LF_IGNORE_LIGATURE)) == 0) { + return ignoreMark; + } else { + return getCombinedIgnoreTester(flags); + } + } + return null; + } + + /** + * Obtain an ignored glyph tester that corresponds to the specified multiple (combined) lookup flags. + * + * @param flags lookup flags + * @return a glyph tester + */ + public GlyphTester getCombinedIgnoreTester(int flags) { + GlyphTester[] gta = new GlyphTester[3]; + int ngt = 0; + if ((flags & GlyphSubtable.LF_IGNORE_BASE) != 0) { + gta[ngt++] = ignoreBase; + } + if ((flags & GlyphSubtable.LF_IGNORE_LIGATURE) != 0) { + gta[ngt++] = ignoreLigature; + } + if ((flags & GlyphSubtable.LF_IGNORE_MARK) != 0) { + gta[ngt++] = ignoreMark; + } + return getCombinedOrTester(gta, ngt); + } + + /** + * Obtain an combined OR glyph tester. + * + * @param gta an array of glyph testers + * @param ngt number of glyph testers present in specified array + * @return a combined OR glyph tester + */ + public GlyphTester getCombinedOrTester(GlyphTester[] gta, int ngt) { + if (ngt > 0) { + return new CombinedOrGlyphTester(gta, ngt); + } else { + return null; + } + } + + /** + * Obtain an combined AND glyph tester. + * + * @param gta an array of glyph testers + * @param ngt number of glyph testers present in specified array + * @return a combined AND glyph tester + */ + public GlyphTester getCombinedAndTester(GlyphTester[] gta, int ngt) { + if (ngt > 0) { + return new CombinedAndGlyphTester(gta, ngt); + } else { + return null; + } + } + + /** + * combined OR glyph tester + */ + private static class CombinedOrGlyphTester implements GlyphTester { + + private GlyphTester[] gta; + private int ngt; + + CombinedOrGlyphTester(GlyphTester[] gta, int ngt) { + this.gta = gta; + this.ngt = ngt; + } + + /** + * {@inheritDoc} + */ + public boolean test(int gi, int flags) { + for (int i = 0, n = ngt; i < n; i++) { + GlyphTester gt = gta[i]; + if (gt != null) { + if (gt.test(gi, flags)) { + return true; + } + } + } + return false; + } + } + + /** + * combined AND glyph tester + */ + private static class CombinedAndGlyphTester implements GlyphTester { + + private GlyphTester[] gta; + private int ngt; + + CombinedAndGlyphTester(GlyphTester[] gta, int ngt) { + this.gta = gta; + this.ngt = ngt; + } + + /** + * {@inheritDoc} + */ + public boolean test(int gi, int flags) { + for (int i = 0, n = ngt; i < n; i++) { + GlyphTester gt = gta[i]; + if (gt != null) { + if (!gt.test(gi, flags)) { + return false; + } + } + } + return true; + } + } + + /** + * NOT glyph tester + */ + private static class NotGlyphTester implements GlyphTester { + + private GlyphTester gt; + + NotGlyphTester(GlyphTester gt) { + this.gt = gt; + } + + /** + * {@inheritDoc} + */ + public boolean test(int gi, int flags) { + if (gt != null) { + if (gt.test(gi, flags)) { + return false; + } + } + return true; + } + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Positionable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Positionable.java new file mode 100644 index 0000000..c2d31d0 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Positionable.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +/** + *

Optional interface which indicates that glyph positioning is supported and, if supported, + * can perform positioning.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface Positionable { + + /** + * Determines if font performs glyph positioning. + * + * @return true if performs positioning + */ + boolean performsPositioning(); + + /** + * Perform glyph positioning. + * + * @param cs character sequence to map to position offsets (advancement adjustments) + * @param script a script identifier + * @param language a language identifier + * @param fontSize font size + * @return array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence, or null if no non-zero adjustment applies + */ + int[][] performPositioning(CharSequence cs, String script, String language, int fontSize); + + /** + * Perform glyph positioning using an implied font size. + * + * @param cs character sequence to map to position offsets (advancement adjustments) + * @param script a script identifier + * @param language a language identifier + * @return array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence, or null if no non-zero adjustment applies + */ + int[][] performPositioning(CharSequence cs, String script, String language); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Substitutable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Substitutable.java new file mode 100644 index 0000000..3e43dfe --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/fonts/Substitutable.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.fonts; + +import java.util.List; + +/** + *

Optional interface which indicates that glyph substitution is supported and, if supported, + * can perform substitution.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface Substitutable { + + /** + * Determines if font performs glyph substitution. + * + * @return true if performs substitution. + */ + boolean performsSubstitution(); + + /** + * Perform substitutions on characters to effect glyph substitution. If some substitution is performed, it + * entails mapping from one or more input characters denoting textual character information to one or more + * output character codes denoting glyphs in this font, where the output character codes may make use of + * private character code values that have significance only for this font. + * + * @param cs character sequence to map to output font encoding character sequence + * @param script a script identifier + * @param language a language identifier + * @param associations optional list to receive list of character associations + * @param retainControls if true, then retain control characters and their glyph mappings, otherwise remove + * @return output sequence (represented as a character sequence, where each character in the returned sequence + * denotes "font characters", i.e., character codes that map directly (1-1) to their associated glyphs + */ + CharSequence performSubstitution(CharSequence cs, String script, String language, List associations, + boolean retainControls); + + /** + * Reorder combining marks in character sequence so that they precede (within the sequence) the base + * character to which they are applied. N.B. In the case of LTR segments, marks are not reordered by this, + * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede + * their base character. + * + * @param cs character sequence within which combining marks to be reordered + * @param gpa associated glyph position adjustments (also reordered) + * @param script a script identifier + * @param language a language identifier + * @param associations optional list of associations to be reordered + * @return output sequence containing reordered "font characters" + */ + CharSequence reorderCombiningMarks(CharSequence cs, int[][] gpa, String script, String language, List associations); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ArabicScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ArabicScriptProcessor.java new file mode 100644 index 0000000..f17013c --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ArabicScriptProcessor.java @@ -0,0 +1,531 @@ +/* + * Copyright (C) 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. + * + */ + +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; + +/** + *

The ArabicScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Arabic script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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/**/ testerMap = new HashMap/**/(); + + 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/**/ testerMap = new HashMap/**/(); + + public GlyphContextTester getTester(String feature) { + return (GlyphContextTester) testerMap.get(feature); + } + } + + private final ScriptContextTester subContextTester; + private final ScriptContextTester posContextTester; + + ArabicScriptProcessor(String script) { + super(script); + this.subContextTester = new SubstitutionScriptContextTester(); + this.posContextTester = new PositioningScriptContextTester(); + } + + /** + * {@inheritDoc} + */ + public String[] getSubstitutionFeatures() { + return GSUB_FEATURES; + } + + /** + * {@inheritDoc} + */ + public ScriptContextTester getSubstitutionContextTester() { + return subContextTester; + } + + /** + * {@inheritDoc} + */ + public String[] getPositioningFeatures() { + return GPOS_FEATURES; + } + + /** + * {@inheritDoc} + */ + public ScriptContextTester getPositioningContextTester() { + return posContextTester; + } + + /** + * {@inheritDoc} + */ + @Override + public GlyphSequence reorderCombiningMarks(GlyphDefinitionTable gdef, GlyphSequence gs, int[] widths, int[][] gpa, + String script, String language) { + // a side effect of BIDI reordering is to order combining marks before their base, so we need to override the default here to + // prevent double reordering + return gs; + } + + private static boolean inFinalContext(String script, String language, String feature, GlyphSequence gs, int index, + int flags) { + CharAssociation a = gs.getAssociation(index); + int[] ca = gs.getCharacterArray(false); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if (!hasFinalPrecedingContext(ca, nc, s, e)) { + return false; + } else if (!hasFinalThisContext(ca, nc, s, e)) { + return false; + } else if (forceFinalThisContext(ca, nc, s, e)) { + return true; + } else return hasFinalSucceedingContext(ca, nc, s, e); + } + } + + private static boolean inInitialContext(String script, String language, String feature, GlyphSequence gs, int index, + int flags) { + CharAssociation a = gs.getAssociation(index); + int[] ca = gs.getCharacterArray(false); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if (!hasInitialPrecedingContext(ca, nc, s, e)) { + return false; + } else if (!hasInitialThisContext(ca, nc, s, e)) { + return false; + } else return hasInitialSucceedingContext(ca, nc, s, e); + } + } + + private static boolean inIsolateContext(String script, String language, String feature, GlyphSequence gs, int index, + int flags) { + CharAssociation a = gs.getAssociation(index); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return false; + } else return (a.getStart() == 0) && (a.getEnd() == nc); + } + + private static boolean inLigatureContext(String script, String language, String feature, GlyphSequence gs, int index, + int flags) { + CharAssociation a = gs.getAssociation(index); + int[] ca = gs.getCharacterArray(false); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if (!hasLigaturePrecedingContext(ca, nc, s, e)) { + return false; + } else return hasLigatureSucceedingContext(ca, nc, s, e); + } + } + + private static boolean inMedialContext(String script, String language, String feature, GlyphSequence gs, int index, + int flags) { + CharAssociation a = gs.getAssociation(index); + int[] ca = gs.getCharacterArray(false); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if (!hasMedialPrecedingContext(ca, nc, s, e)) { + return false; + } else if (!hasMedialThisContext(ca, nc, s, e)) { + return false; + } else return hasMedialSucceedingContext(ca, nc, s, e); + } + } + + private static boolean hasFinalPrecedingContext(int[] ca, int nc, int s, int e) { + int chp = 0; // preceding non-NSM char in [0,s) searching back from s + int clp = 0; + for (int i = s; i > 0; i--) { + int k = i - 1; + if ((k >= 0) && (k < nc)) { + chp = ca[k]; + clp = BidiClass.getBidiClass(chp); + if (clp != BidiConstants.NSM) { + break; + } + } + } + if (clp != BidiConstants.AL) { + return isZWJ(chp); + } else return !hasIsolateInitial(chp); + } + + private static boolean hasFinalThisContext(int[] ca, int nc, int s, int e) { + int chl = 0; // last non-{NSM,ZWJ} char in [s,e) + int cll = 0; + for (int i = 0, n = e - s; i < n; i++) { + int k = n - i - 1; + int j = s + k; + if ((j >= 0) && (j < nc)) { + chl = ca[j]; + cll = BidiClass.getBidiClass(chl); + if ((cll != BidiConstants.NSM) && !isZWJ(chl)) { + break; + } + } + } + if (cll != BidiConstants.AL) { + return false; + } + return !hasIsolateFinal(chl); + } + + private static boolean forceFinalThisContext(int[] ca, int nc, int s, int e) { + int chl = 0; // last non-{NSM,ZWJ} char in [s,e) + int cll = 0; + for (int i = 0, n = e - s; i < n; i++) { + int k = n - i - 1; + int j = s + k; + if ((j >= 0) && (j < nc)) { + chl = ca[j]; + cll = BidiClass.getBidiClass(chl); + if ((cll != BidiConstants.NSM) && !isZWJ(chl)) { + break; + } + } + } + if (cll != BidiConstants.AL) { + return false; + } + return hasIsolateInitial(chl); + } + + private static boolean hasFinalSucceedingContext(int[] ca, int nc, int s, int e) { + int chs = 0; // succeeding non-NSM char in [e,nc) searching forward from e + int cls = 0; + for (int i = e, n = nc; i < n; i++) { + chs = ca[i]; + cls = BidiClass.getBidiClass(chs); + if (cls != BidiConstants.NSM) { + break; + } + } + if (cls != BidiConstants.AL) { + return !isZWJ(chs); + } else return hasIsolateFinal(chs); + } + + private static boolean hasInitialPrecedingContext(int[] ca, int nc, int s, int e) { + int chp = 0; // preceding non-NSM char in [0,s) searching back from s + int clp = 0; + for (int i = s; i > 0; i--) { + int k = i - 1; + if ((k >= 0) && (k < nc)) { + chp = ca[k]; + clp = BidiClass.getBidiClass(chp); + if (clp != BidiConstants.NSM) { + break; + } + } + } + if (clp != BidiConstants.AL) { + return !isZWJ(chp); + } else return hasIsolateInitial(chp); + } + + private static boolean hasInitialThisContext(int[] ca, int nc, int s, int e) { + int chf = 0; // first non-{NSM,ZWJ} char in [s,e) + int clf = 0; + for (int i = 0, n = e - s; i < n; i++) { + int k = s + i; + if ((k >= 0) && (k < nc)) { + chf = ca[s + i]; + clf = BidiClass.getBidiClass(chf); + if ((clf != BidiConstants.NSM) && !isZWJ(chf)) { + break; + } + } + } + if (clf != BidiConstants.AL) { + return false; + } + return !hasIsolateInitial(chf); + } + + private static boolean hasInitialSucceedingContext(int[] ca, int nc, int s, int e) { + int chs = 0; // succeeding non-NSM char in [e,nc) searching forward from e + int cls = 0; + for (int i = e, n = nc; i < n; i++) { + chs = ca[i]; + cls = BidiClass.getBidiClass(chs); + if (cls != BidiConstants.NSM) { + break; + } + } + if (cls != BidiConstants.AL) { + return isZWJ(chs); + } else return !hasIsolateFinal(chs); + } + + private static boolean hasMedialPrecedingContext(int[] ca, int nc, int s, int e) { + int chp = 0; // preceding non-NSM char in [0,s) searching back from s + int clp = 0; + for (int i = s; i > 0; i--) { + int k = i - 1; + if ((k >= 0) && (k < nc)) { + chp = ca[k]; + clp = BidiClass.getBidiClass(chp); + if (clp != BidiConstants.NSM) { + break; + } + } + } + if (clp != BidiConstants.AL) { + return isZWJ(chp); + } else return !hasIsolateInitial(chp); + } + + private static boolean hasMedialThisContext(int[] ca, int nc, int s, int e) { + int chf = 0; // first non-{NSM,ZWJ} char in [s,e) + int clf = 0; + for (int i = 0, n = e - s; i < n; i++) { + int k = s + i; + if ((k >= 0) && (k < nc)) { + chf = ca[s + i]; + clf = BidiClass.getBidiClass(chf); + if ((clf != BidiConstants.NSM) && !isZWJ(chf)) { + break; + } + } + } + if (clf != BidiConstants.AL) { + return false; + } + int chl = 0; // last non-{NSM,ZWJ} char in [s,e) + int cll = 0; + for (int i = 0, n = e - s; i < n; i++) { + int k = n - i - 1; + int j = s + k; + if ((j >= 0) && (j < nc)) { + chl = ca[j]; + cll = BidiClass.getBidiClass(chl); + if ((cll != BidiConstants.NSM) && !isZWJ(chl)) { + break; + } + } + } + if (cll != BidiConstants.AL) { + return false; + } + if (hasIsolateFinal(chf)) { + return false; + } else return !hasIsolateInitial(chl); + } + + private static boolean hasMedialSucceedingContext(int[] ca, int nc, int s, int e) { + int chs = 0; // succeeding non-NSM char in [e,nc) searching forward from e + int cls = 0; + for (int i = e, n = nc; i < n; i++) { + chs = ca[i]; + cls = BidiClass.getBidiClass(chs); + if (cls != BidiConstants.NSM) { + break; + } + } + if (cls != BidiConstants.AL) { + return isZWJ(chs); + } else return !hasIsolateFinal(chs); + } + + private static boolean hasLigaturePrecedingContext(int[] ca, int nc, int s, int e) { + return true; + } + + private static boolean hasLigatureSucceedingContext(int[] ca, int nc, int s, int e) { + int chs = 0; // succeeding non-NSM char in [e,nc) searching forward from e + int cls = 0; + for (int i = e, n = nc; i < n; i++) { + chs = ca[i]; + cls = BidiClass.getBidiClass(chs); + // TBD - does ZWJ have impact here? + if (cls != BidiConstants.NSM) { + break; + } + } + return cls == BidiConstants.AL; + } + + /** + * Ordered array of Unicode scalars designating those Arabic (Script) Letters + * which exhibit an isolated form in word initial position. + */ + private static final int[] ISOLATED_INITIALS = { + 0x0621, // HAMZA + 0x0622, // ALEF WITH MADDA ABOVE + 0x0623, // ALEF WITH HAMZA ABOVE + 0x0624, // WAW WITH HAMZA ABOVE + 0x0625, // ALEF WITH HAMZA BELOWW + 0x0627, // ALEF + 0x062F, // DAL + 0x0630, // THAL + 0x0631, // REH + 0x0632, // ZAIN + 0x0648, // WAW + 0x0671, // ALEF WASLA + 0x0672, // ALEF WITH WAVY HAMZA ABOVE + 0x0673, // ALEF WITH WAVY HAMZA BELOW + 0x0675, // HIGH HAMZA ALEF + 0x0676, // HIGH HAMZA WAW + 0x0677, // U WITH HAMZA ABOVE + 0x0688, // DDAL + 0x0689, // DAL WITH RING + 0x068A, // DAL WITH DOT BELOW + 0x068B, // DAL WITH DOT BELOW AND SMALL TAH + 0x068C, // DAHAL + 0x068D, // DDAHAL + 0x068E, // DUL + 0x068F, // DUL WITH THREE DOTS ABOVE DOWNWARDS + 0x0690, // DUL WITH FOUR DOTS ABOVE + 0x0691, // RREH + 0x0692, // REH WITH SMALL V + 0x0693, // REH WITH RING + 0x0694, // REH WITH DOT BELOW + 0x0695, // REH WITH SMALL V BELOW + 0x0696, // REH WITH DOT BELOW AND DOT ABOVE + 0x0697, // REH WITH TWO DOTS ABOVE + 0x0698, // JEH + 0x0699, // REH WITH FOUR DOTS ABOVE + 0x06C4, // WAW WITH RING + 0x06C5, // KIRGHIZ OE + 0x06C6, // OE + 0x06C7, // U + 0x06C8, // YU + 0x06C9, // KIRGHIZ YU + 0x06CA, // WAW WITH TWO DOTS ABOVE + 0x06CB, // VE + 0x06CF, // WAW WITH DOT ABOVE + 0x06EE, // DAL WITH INVERTED V + 0x06EF // REH WITH INVERTED V + }; + + private static boolean hasIsolateInitial(int ch) { + return Arrays.binarySearch(ISOLATED_INITIALS, ch) >= 0; + } + + /** + * Ordered array of Unicode scalars designating those Arabic (Script) Letters + * which exhibit an isolated form in word final position. + */ + private static final int[] ISOLATED_FINALS = { + 0x0621 // HAMZA + }; + + private static boolean hasIsolateFinal(int ch) { + return Arrays.binarySearch(ISOLATED_FINALS, ch) >= 0; + } + + private static boolean isZWJ(int ch) { + return ch == CharUtilities.ZERO_WIDTH_JOINER; + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DefaultScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DefaultScriptProcessor.java new file mode 100644 index 0000000..d94fbde --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DefaultScriptProcessor.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 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. + * + */ + +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; + +/** + *

Default script processor, which enables default glyph composition/decomposition, common ligatures, localized + * forms + * and kerning.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class DefaultScriptProcessor extends ScriptProcessor { + + /** + * features to use for substitutions + */ + private static final String[] GSUB_FEATURES = { + "ccmp", // glyph composition/decomposition + "liga", // common ligatures + "locl" // localized forms + }; + + /** + * features to use for positioning + */ + private static final String[] GPOS_FEATURES = { + "kern", // kerning + "mark", // mark to base or ligature positioning + "mkmk" // mark to mark positioning + }; + + DefaultScriptProcessor(String script) { + super(script); + } + + @Override + /** {@inheritDoc} */ + public String[] getSubstitutionFeatures() { + return GSUB_FEATURES; + } + + @Override + /** {@inheritDoc} */ + public ScriptContextTester getSubstitutionContextTester() { + return null; + } + + @Override + /** {@inheritDoc} */ + public String[] getPositioningFeatures() { + return GPOS_FEATURES; + } + + @Override + /** {@inheritDoc} */ + public ScriptContextTester getPositioningContextTester() { + return null; + } + + @Override + /** {@inheritDoc} */ + public GlyphSequence reorderCombiningMarks(GlyphDefinitionTable gdef, GlyphSequence gs, int[] unscaledWidths, + int[][] gpa, String script, String language) { + int ng = gs.getGlyphCount(); + int[] ga = gs.getGlyphArray(false); + int nm = 0; + // count combining marks + for (int i = 0; i < ng; i++) { + int gid = ga[i]; + int gw = unscaledWidths[i]; + if (isReorderedMark(gdef, ga, unscaledWidths, i)) { + nm++; + } + } + // only reorder if there is at least one mark and at least one non-mark glyph + if ((nm > 0) && ((ng - nm) > 0)) { + CharAssociation[] aa = gs.getAssociations(0, -1); + int[] nga = new int[ng]; + int[][] npa = (gpa != null) ? new int[ng][] : null; + CharAssociation[] naa = new CharAssociation[ng]; + int k = 0; + CharAssociation ba = null; + int bg = -1; + int[] bpa = null; + for (int i = 0; i < ng; i++) { + int gid = ga[i]; + int[] pa = (gpa != null) ? gpa[i] : null; + CharAssociation ca = aa[i]; + if (isReorderedMark(gdef, ga, unscaledWidths, i)) { + nga[k] = gid; + naa[k] = ca; + if (npa != null) { + npa[k] = pa; + } + k++; + } else { + if (bg != -1) { + nga[k] = bg; + naa[k] = ba; + if (npa != null) { + npa[k] = bpa; + } + k++; + bg = -1; + ba = null; + bpa = null; + } + if (bg == -1) { + bg = gid; + ba = ca; + bpa = pa; + } + } + } + if (bg != -1) { + nga[k] = bg; + naa[k] = ba; + if (npa != null) { + npa[k] = bpa; + } + k++; + } + assert k == ng; + if (npa != null) { + System.arraycopy(npa, 0, gpa, 0, ng); + } + return new GlyphSequence(gs, null, nga, null, null, naa, null); + } else { + return gs; + } + } + + protected boolean isReorderedMark(GlyphDefinitionTable gdef, int[] glyphs, int[] unscaledWidths, int index) { + return gdef.isGlyphClass(glyphs[index], GlyphDefinitionTable.GLYPH_CLASS_MARK) && (unscaledWidths[index] != 0); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DevanagariScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DevanagariScriptProcessor.java new file mode 100644 index 0000000..023a1b7 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/DevanagariScriptProcessor.java @@ -0,0 +1,550 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.scripts; + +// CSOFF: LineLengthCheck + +import com.jaredrummler.fontreader.complexscripts.util.CharAssociation; +import com.jaredrummler.fontreader.util.GlyphSequence; + +/** + *

The DevanagariScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Devanagari script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class DevanagariScriptProcessor extends IndicScriptProcessor { + + DevanagariScriptProcessor(String script) { + super(script); + } + + @Override + protected Class getSyllabizerClass() { + return DevanagariSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = ng; i > 0; i--) { + int k = i - 1; + if (containsPreBaseMatra(gs, k)) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = (source < ng) ? source : ng; i > 0; i--) { + int k = i - 1; + if (containsConsonant(gs, k)) { + if (containsHalfConsonant(gs, k)) { + lk = k; + } else if (lk == -1) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isPreM(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsConsonant(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isC(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant(GlyphSequence gs, int k) { + Boolean half = (Boolean) gs.getAssociation(k).getPredication("half"); + return (half != null) ? half.booleanValue() : false; + } + + @Override + protected int findReph(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int li = -1; + for (int i = 0; i < ng; i++) { + if (containsReph(gs, i)) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for (int i = 0; i < ng; i++) { + if ((i != source) && containsConsonant(gs, i)) { + if (!containsHalfConsonant(gs, i)) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) { + if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) { + c2 = i + 1; + } else if (containsOtherMark(gs, i)) { + c2 = i; + break; + } + } + if (c2 >= 0) { + return c2; + } else if (c1 >= 0) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph(GlyphSequence gs, int k) { + Boolean rphf = (Boolean) gs.getAssociation(k).getPredication("rphf"); + return (rphf != null) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isM(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + switch (typeOf(ca[i])) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class DevanagariSyllabizer extends DefaultSyllabizer { + + DevanagariSyllabizer(String script, String language) { + super(script, language); + } + + @Override + // | C ... + protected int findStartOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + while (s < e) { + int c = ca[s]; + if (isC(c)) { + break; + } else { + s++; + } + } + return s; + } + } + + @Override + // D* L? | ... + protected int findEndOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ((i = isDeadConsonant(ca, s, e)) > s) { + s = i; + nd++; + } + // consume zero or one live consonant + if ((i = isLiveConsonant(ca, s, e)) > s) { + s = i; + nl++; + } + return ((nd > 0) || (nl > 0)) ? s : -1; + } + } + + // D := ( C N? H )? + private int isDeadConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nh = 0; + do { + // C + if ((s + i) < e) { + c = ca[s + i]; + if (isC(c)) { + i++; + nc++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca[s + 1]; + if (isN(c)) { + i++; + } + } + // H + if ((s + i) < e) { + c = ca[s + i]; + if (isH(c)) { + i++; + nh++; + } else { + break; + } + } + } while (false); + return (nc > 0) && (nh > 0) ? s + i : -1; + } + } + + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nv = 0; + int nx = 0; + do { + // C + if ((s + i) < e) { + c = ca[s + i]; + if (isC(c)) { + i++; + nc++; + } else if (isV(c)) { + i++; + nv++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca[s + i]; + if (isN(c)) { + i++; + } + } + // X* + while ((s + i) < e) { + c = ca[s + i]; + if (isX(c)) { + i++; + nx++; + } else { + break; + } + } + } while (false); + // if no X but has H, then ignore C|I + if (nx == 0) { + if ((s + i) < e) { + c = ca[s + i]; + if (isH(c)) { + if (nc > 0) { + nc--; + } else if (nv > 0) { + nv--; + } + } + } + } + return ((nc > 0) || (nv > 0)) ? s + i : -1; + } + } + } + + // devanagari character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // devanagari block range + static final int CCA_START = 0x0900; // first code point mapped by cca + static final int CCA_END = 0x0980; // last code point + 1 mapped by cca + // devanagari character type lookups + static final short[] CCA = { + C_O, // 0x0900 // INVERTED CANDRABINDU + C_O, // 0x0901 // CANDRABINDU + C_O, // 0x0902 // ANUSVARA + C_O, // 0x0903 // VISARGA + C_V, // 0x0904 // SHORT A + C_V, // 0x0905 // A + C_V, // 0x0906 // AA + C_V, // 0x0907 // I + C_V, // 0x0908 // II + C_V, // 0x0909 // U + C_V, // 0x090A // UU + C_V, // 0x090B // VOCALIC R + C_V, // 0x090C // VOCALIC L + C_V, // 0x090D // CANDRA E + C_V, // 0x090E // SHORT E + C_V, // 0x090F // E + C_V, // 0x0910 // AI + C_V, // 0x0911 // CANDRA O + C_V, // 0x0912 // SHORT O + C_V, // 0x0913 // O + C_V, // 0x0914 // AU + C_C, // 0x0915 // KA + C_C, // 0x0916 // KHA + C_C, // 0x0917 // GA + C_C, // 0x0918 // GHA + C_C, // 0x0919 // NGA + C_C, // 0x091A // CA + C_C, // 0x091B // CHA + C_C, // 0x091C // JA + C_C, // 0x091D // JHA + C_C, // 0x091E // NYA + C_C, // 0x091F // TTA + C_C, // 0x0920 // TTHA + C_C, // 0x0921 // DDA + C_C, // 0x0922 // DDHA + C_C, // 0x0923 // NNA + C_C, // 0x0924 // TA + C_C, // 0x0925 // THA + C_C, // 0x0926 // DA + C_C, // 0x0927 // DHA + C_C, // 0x0928 // NA + C_C, // 0x0929 // NNNA + C_C, // 0x092A // PA + C_C, // 0x092B // PHA + C_C, // 0x092C // BA + C_C, // 0x092D // BHA + C_C, // 0x092E // MA + C_C, // 0x092F // YA + C_C | C_R, // 0x0930 // RA + C_C | C_R | C_N, // 0x0931 // RRA = 0930+093C + C_C, // 0x0932 // LA + C_C, // 0x0933 // LLA + C_C, // 0x0934 // LLLA + C_C, // 0x0935 // VA + C_C, // 0x0936 // SHA + C_C, // 0x0937 // SSA + C_C, // 0x0938 // SA + C_C, // 0x0939 // HA + C_M, // 0x093A // OE (KASHMIRI) + C_M, // 0x093B // OOE (KASHMIRI) + C_N, // 0x093C // NUKTA + C_S, // 0x093D // AVAGRAHA + C_M, // 0x093E // AA + C_M | C_PRE, // 0x093F // I + C_M, // 0x0940 // II + C_M, // 0x0941 // U + C_M, // 0x0942 // UU + C_M, // 0x0943 // VOCALIC R + C_M, // 0x0944 // VOCALIC RR + C_M, // 0x0945 // CANDRA E + C_M, // 0x0946 // SHORT E + C_M, // 0x0947 // E + C_M, // 0x0948 // AI + C_M, // 0x0949 // CANDRA O + C_M, // 0x094A // SHORT O + C_M, // 0x094B // O + C_M, // 0x094C // AU + C_H, // 0x094D // VIRAMA (HALANT) + C_M, // 0x094E // PRISHTHAMATRA E + C_M, // 0x094F // AW + C_S, // 0x0950 // OM + C_T, // 0x0951 // UDATTA + C_T, // 0x0952 // ANUDATTA + C_A, // 0x0953 // GRAVE + C_A, // 0x0954 // ACUTE + C_M, // 0x0955 // CANDRA LONG E + C_M, // 0x0956 // UE + C_M, // 0x0957 // UUE + C_C | C_N, // 0x0958 // QA + C_C | C_N, // 0x0959 // KHHA + C_C | C_N, // 0x095A // GHHA + C_C | C_N, // 0x095B // ZA + C_C | C_N, // 0x095C // DDDHA + C_C | C_N, // 0x095D // RHA + C_C | C_N, // 0x095E // FA + C_C | C_N, // 0x095F // YYA + C_V, // 0x0960 // VOCALIC RR + C_V, // 0x0961 // VOCALIC LL + C_M, // 0x0962 // VOCALIC RR + C_M, // 0x0963 // VOCALIC LL + C_P, // 0x0964 // DANDA + C_P, // 0x0965 // DOUBLE DANDA + C_D, // 0x0966 // ZERO + C_D, // 0x0967 // ONE + C_D, // 0x0968 // TWO + C_D, // 0x0969 // THREE + C_D, // 0x096A // FOUR + C_D, // 0x096B // FIVE + C_D, // 0x096C // SIX + C_D, // 0x096D // SEVEN + C_D, // 0x096E // EIGHT + C_D, // 0x096F // NINE + C_S, // 0x0970 // ABBREVIATION SIGN + C_S, // 0x0971 // HIGH SPACING DOT + C_V, // 0x0972 // CANDRA A (MARATHI) + C_V, // 0x0973 // OE (KASHMIRI) + C_V, // 0x0974 // OOE (KASHMIRI) + C_V, // 0x0975 // AW (KASHMIRI) + C_V, // 0x0976 // UE (KASHMIRI) + C_V, // 0x0977 // UUE (KASHMIRI) + C_U, // 0x0978 // UNASSIGNED + C_C, // 0x0979 // ZHA + C_C, // 0x097A // HEAVY YA + C_C, // 0x097B // GGAA (SINDHI) + C_C, // 0x097C // JJA (SINDHI) + C_C, // 0x097D // GLOTTAL STOP (LIMBU) + C_C, // 0x097E // DDDA (SINDHI) + C_C // 0x097F // BBA (SINDHI) + }; + + static int typeOf(int c) { + if ((c >= CCA_START) && (c < CCA_END)) { + return CCA[c - CCA_START] & C_M_TYPE; + } else { + return C_U; + } + } + + static boolean isType(int c, int t) { + return typeOf(c) == t; + } + + static boolean hasFlag(int c, int f) { + if ((c >= CCA_START) && (c < CCA_END)) { + return (CCA[c - CCA_START] & f) == f; + } else { + return false; + } + } + + static boolean isC(int c) { + return isType(c, C_C); + } + + static boolean isR(int c) { + return isType(c, C_C) && hasR(c); + } + + static boolean isV(int c) { + return isType(c, C_V); + } + + static boolean isN(int c) { + return c == 0x093C; + } + + static boolean isH(int c) { + return c == 0x094D; + } + + static boolean isM(int c) { + return isType(c, C_M); + } + + static boolean isPreM(int c) { + return isType(c, C_M) && hasFlag(c, C_PRE); + } + + static boolean isX(int c) { + switch (typeOf(c)) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + + static boolean hasR(int c) { + return hasFlag(c, C_R); + } + + static boolean hasN(int c) { + return hasFlag(c, C_N); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GujaratiScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GujaratiScriptProcessor.java new file mode 100644 index 0000000..a524596 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GujaratiScriptProcessor.java @@ -0,0 +1,548 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.scripts; + +import com.jaredrummler.fontreader.complexscripts.util.CharAssociation; +import com.jaredrummler.fontreader.util.GlyphSequence; + +/** + *

The GujaratiScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Gujarati script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class GujaratiScriptProcessor extends IndicScriptProcessor { + + GujaratiScriptProcessor(String script) { + super(script); + } + + @Override + protected Class getSyllabizerClass() { + return GujaratiSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = ng; i > 0; i--) { + int k = i - 1; + if (containsPreBaseMatra(gs, k)) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = (source < ng) ? source : ng; i > 0; i--) { + int k = i - 1; + if (containsConsonant(gs, k)) { + if (containsHalfConsonant(gs, k)) { + lk = k; + } else if (lk == -1) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isPreM(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsConsonant(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isC(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant(GlyphSequence gs, int k) { + Boolean half = (Boolean) gs.getAssociation(k).getPredication("half"); + return (half != null) ? half.booleanValue() : false; + } + + @Override + protected int findReph(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int li = -1; + for (int i = 0; i < ng; i++) { + if (containsReph(gs, i)) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for (int i = 0; i < ng; i++) { + if ((i != source) && containsConsonant(gs, i)) { + if (!containsHalfConsonant(gs, i)) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) { + if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) { + c2 = i + 1; + } else if (containsOtherMark(gs, i)) { + c2 = i; + break; + } + } + if (c2 >= 0) { + return c2; + } else if (c1 >= 0) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph(GlyphSequence gs, int k) { + Boolean rphf = (Boolean) gs.getAssociation(k).getPredication("rphf"); + return (rphf != null) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isM(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + switch (typeOf(ca[i])) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class GujaratiSyllabizer extends DefaultSyllabizer { + + GujaratiSyllabizer(String script, String language) { + super(script, language); + } + + @Override + // | C ... + protected int findStartOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + while (s < e) { + int c = ca[s]; + if (isC(c)) { + break; + } else { + s++; + } + } + return s; + } + } + + @Override + // D* L? | ... + protected int findEndOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ((i = isDeadConsonant(ca, s, e)) > s) { + s = i; + nd++; + } + // consume zero or one live consonant + if ((i = isLiveConsonant(ca, s, e)) > s) { + s = i; + nl++; + } + return ((nd > 0) || (nl > 0)) ? s : -1; + } + } + + // D := ( C N? H )? + private int isDeadConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nh = 0; + do { + // C + if ((s + i) < e) { + c = ca[s + i]; + if (isC(c)) { + i++; + nc++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca[s + 1]; + if (isN(c)) { + i++; + } + } + // H + if ((s + i) < e) { + c = ca[s + i]; + if (isH(c)) { + i++; + nh++; + } else { + break; + } + } + } while (false); + return (nc > 0) && (nh > 0) ? s + i : -1; + } + } + + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nv = 0; + int nx = 0; + do { + // C + if ((s + i) < e) { + c = ca[s + i]; + if (isC(c)) { + i++; + nc++; + } else if (isV(c)) { + i++; + nv++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca[s + i]; + if (isN(c)) { + i++; + } + } + // X* + while ((s + i) < e) { + c = ca[s + i]; + if (isX(c)) { + i++; + nx++; + } else { + break; + } + } + } while (false); + // if no X but has H, then ignore C|I + if (nx == 0) { + if ((s + i) < e) { + c = ca[s + i]; + if (isH(c)) { + if (nc > 0) { + nc--; + } else if (nv > 0) { + nv--; + } + } + } + } + return ((nc > 0) || (nv > 0)) ? s + i : -1; + } + } + } + + // gujarati character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // gujarati block range + static final int CCA_START = 0x0A80; // first code point mapped by cca + static final int CCA_END = 0x0B00; // last code point + 1 mapped by cca + // gujarati character type lookups + static final short[] CCA = { + C_U, // 0x0A80 // UNASSIGNED + C_O, // 0x0A81 // CANDRABINDU + C_O, // 0x0A82 // ANUSVARA + C_O, // 0x0A83 // VISARGA + C_U, // 0x0A84 // UNASSIGNED + C_V, // 0x0A85 // A + C_V, // 0x0A86 // AA + C_V, // 0x0A87 // I + C_V, // 0x0A88 // II + C_V, // 0x0A89 // U + C_V, // 0x0A8A // UU + C_V, // 0x0A8B // VOCALIC R + C_V, // 0x0A8C // VOCALIC L + C_V, // 0x0A8D // CANDRA E + C_U, // 0x0A8E // UNASSIGNED + C_V, // 0x0A8F // E + C_V, // 0x0A90 // AI + C_V, // 0x0A91 // CANDRA O + C_U, // 0x0A92 // UNASSIGNED + C_V, // 0x0A93 // O + C_V, // 0x0A94 // AU + C_C, // 0x0A95 // KA + C_C, // 0x0A96 // KHA + C_C, // 0x0A97 // GA + C_C, // 0x0A98 // GHA + C_C, // 0x0A99 // NGA + C_C, // 0x0A9A // CA + C_C, // 0x0A9B // CHA + C_C, // 0x0A9C // JA + C_C, // 0x0A9D // JHA + C_C, // 0x0A9E // NYA + C_C, // 0x0A9F // TTA + C_C, // 0x0AA0 // TTHA + C_C, // 0x0AA1 // DDA + C_C, // 0x0AA2 // DDHA + C_C, // 0x0AA3 // NNA + C_C, // 0x0AA4 // TA + C_C, // 0x0AA5 // THA + C_C, // 0x0AA6 // DA + C_C, // 0x0AA7 // DHA + C_C, // 0x0AA8 // NA + C_U, // 0x0AA9 // UNASSIGNED + C_C, // 0x0AAA // PA + C_C, // 0x0AAB // PHA + C_C, // 0x0AAC // BA + C_C, // 0x0AAD // BHA + C_C, // 0x0AAE // MA + C_C, // 0x0AAF // YA + C_C | C_R, // 0x0AB0 // RA + C_U, // 0x0AB1 // UNASSIGNED + C_C, // 0x0AB2 // LA + C_C, // 0x0AB3 // LLA + C_U, // 0x0AB4 // UNASSIGNED + C_C, // 0x0AB5 // VA + C_C, // 0x0AB6 // SHA + C_C, // 0x0AB7 // SSA + C_C, // 0x0AB8 // SA + C_C, // 0x0AB9 // HA + C_U, // 0x0ABA // UNASSIGNED + C_U, // 0x0ABB // UNASSIGNED + C_N, // 0x0ABC // NUKTA + C_S, // 0x0ABD // AVAGRAHA + C_M, // 0x0ABE // AA + C_M | C_PRE, // 0x0ABF // I + C_M, // 0x0AC0 // II + C_M, // 0x0AC1 // U + C_M, // 0x0AC2 // UU + C_M, // 0x0AC3 // VOCALIC R + C_M, // 0x0AC4 // VOCALIC RR + C_M, // 0x0AC5 // CANDRA E + C_U, // 0x0AC6 // UNASSIGNED + C_M, // 0x0AC7 // E + C_M, // 0x0AC8 // AI + C_M, // 0x0AC9 // CANDRA O + C_U, // 0x0ACA // UNASSIGNED + C_M, // 0x0ACB // O + C_M, // 0x0ACC // AU + C_H, // 0x0ACD // VIRAMA (HALANT) + C_U, // 0x0ACE // UNASSIGNED + C_U, // 0x0ACF // UNASSIGNED + C_S, // 0x0AD0 // OM + C_U, // 0x0AD1 // UNASSIGNED + C_U, // 0x0AD2 // UNASSIGNED + C_U, // 0x0AD3 // UNASSIGNED + C_U, // 0x0AD4 // UNASSIGNED + C_U, // 0x0AD5 // UNASSIGNED + C_U, // 0x0AD6 // UNASSIGNED + C_U, // 0x0AD7 // UNASSIGNED + C_U, // 0x0AD8 // UNASSIGNED + C_U, // 0x0AD9 // UNASSIGNED + C_U, // 0x0ADA // UNASSIGNED + C_U, // 0x0ADB // UNASSIGNED + C_U, // 0x0ADC // UNASSIGNED + C_U, // 0x0ADD // UNASSIGNED + C_U, // 0x0ADE // UNASSIGNED + C_U, // 0x0ADF // UNASSIGNED + C_V, // 0x0AE0 // VOCALIC RR + C_V, // 0x0AE1 // VOCALIC LL + C_M, // 0x0AE2 // VOCALIC L + C_M, // 0x0AE3 // VOCALIC LL + C_U, // 0x0AE4 // UNASSIGNED + C_U, // 0x0AE5 // UNASSIGNED + C_D, // 0x0AE6 // ZERO + C_D, // 0x0AE7 // ONE + C_D, // 0x0AE8 // TWO + C_D, // 0x0AE9 // THREE + C_D, // 0x0AEA // FOUR + C_D, // 0x0AEB // FIVE + C_D, // 0x0AEC // SIX + C_D, // 0x0AED // SEVEN + C_D, // 0x0AEE // EIGHT + C_D, // 0x0AEF // NINE + C_U, // 0x0AF0 // UNASSIGNED + C_S, // 0x0AF1 // RUPEE SIGN + C_U, // 0x0AF2 // UNASSIGNED + C_U, // 0x0AF3 // UNASSIGNED + C_U, // 0x0AF4 // UNASSIGNED + C_U, // 0x0AF5 // UNASSIGNED + C_U, // 0x0AF6 // UNASSIGNED + C_U, // 0x0AF7 // UNASSIGNED + C_U, // 0x0AF8 // UNASSIGNED + C_U, // 0x0AF9 // UNASSIGNED + C_U, // 0x0AFA // UNASSIGNED + C_U, // 0x0AFB // UNASSIGNED + C_U, // 0x0AFC // UNASSIGNED + C_U, // 0x0AFD // UNASSIGNED + C_U, // 0x0AFE // UNASSIGNED + C_U // 0x0AFF // UNASSIGNED + }; + + static int typeOf(int c) { + if ((c >= CCA_START) && (c < CCA_END)) { + return CCA[c - CCA_START] & C_M_TYPE; + } else { + return C_U; + } + } + + static boolean isType(int c, int t) { + return typeOf(c) == t; + } + + static boolean hasFlag(int c, int f) { + if ((c >= CCA_START) && (c < CCA_END)) { + return (CCA[c - CCA_START] & f) == f; + } else { + return false; + } + } + + static boolean isC(int c) { + return isType(c, C_C); + } + + static boolean isR(int c) { + return isType(c, C_C) && hasR(c); + } + + static boolean isV(int c) { + return isType(c, C_V); + } + + static boolean isN(int c) { + return c == 0x0ABC; + } + + static boolean isH(int c) { + return c == 0x0ACD; + } + + static boolean isM(int c) { + return isType(c, C_M); + } + + static boolean isPreM(int c) { + return isType(c, C_M) && hasFlag(c, C_PRE); + } + + static boolean isX(int c) { + switch (typeOf(c)) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + + static boolean hasR(int c) { + return hasFlag(c, C_R); + } + + static boolean hasN(int c) { + return hasFlag(c, C_N); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GurmukhiScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GurmukhiScriptProcessor.java new file mode 100644 index 0000000..9531fd9 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/GurmukhiScriptProcessor.java @@ -0,0 +1,548 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.scripts; + +import com.jaredrummler.fontreader.complexscripts.util.CharAssociation; +import com.jaredrummler.fontreader.util.GlyphSequence; + +/** + *

The GurmukhiScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Gurmukhi script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class GurmukhiScriptProcessor extends IndicScriptProcessor { + + GurmukhiScriptProcessor(String script) { + super(script); + } + + @Override + protected Class getSyllabizerClass() { + return GurmukhiSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = ng; i > 0; i--) { + int k = i - 1; + if (containsPreBaseMatra(gs, k)) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = (source < ng) ? source : ng; i > 0; i--) { + int k = i - 1; + if (containsConsonant(gs, k)) { + if (containsHalfConsonant(gs, k)) { + lk = k; + } else if (lk == -1) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isPreM(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsConsonant(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isC(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant(GlyphSequence gs, int k) { + Boolean half = (Boolean) gs.getAssociation(k).getPredication("half"); + return (half != null) ? half.booleanValue() : false; + } + + @Override + protected int findReph(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int li = -1; + for (int i = 0; i < ng; i++) { + if (containsReph(gs, i)) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for (int i = 0; i < ng; i++) { + if ((i != source) && containsConsonant(gs, i)) { + if (!containsHalfConsonant(gs, i)) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) { + if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) { + c2 = i + 1; + } else if (containsOtherMark(gs, i)) { + c2 = i; + break; + } + } + if (c2 >= 0) { + return c2; + } else if (c1 >= 0) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph(GlyphSequence gs, int k) { + Boolean rphf = (Boolean) gs.getAssociation(k).getPredication("rphf"); + return (rphf != null) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isM(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + switch (typeOf(ca[i])) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class GurmukhiSyllabizer extends DefaultSyllabizer { + + GurmukhiSyllabizer(String script, String language) { + super(script, language); + } + + @Override + // | C ... + protected int findStartOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + while (s < e) { + int c = ca[s]; + if (isC(c)) { + break; + } else { + s++; + } + } + return s; + } + } + + @Override + // D* L? | ... + protected int findEndOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ((i = isDeadConsonant(ca, s, e)) > s) { + s = i; + nd++; + } + // consume zero or one live consonant + if ((i = isLiveConsonant(ca, s, e)) > s) { + s = i; + nl++; + } + return ((nd > 0) || (nl > 0)) ? s : -1; + } + } + + // D := ( C N? H )? + private int isDeadConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nh = 0; + do { + // C + if ((s + i) < e) { + c = ca[s + i]; + if (isC(c)) { + i++; + nc++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca[s + 1]; + if (isN(c)) { + i++; + } + } + // H + if ((s + i) < e) { + c = ca[s + i]; + if (isH(c)) { + i++; + nh++; + } else { + break; + } + } + } while (false); + return (nc > 0) && (nh > 0) ? s + i : -1; + } + } + + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nv = 0; + int nx = 0; + do { + // C + if ((s + i) < e) { + c = ca[s + i]; + if (isC(c)) { + i++; + nc++; + } else if (isV(c)) { + i++; + nv++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca[s + i]; + if (isN(c)) { + i++; + } + } + // X* + while ((s + i) < e) { + c = ca[s + i]; + if (isX(c)) { + i++; + nx++; + } else { + break; + } + } + } while (false); + // if no X but has H, then ignore C|I + if (nx == 0) { + if ((s + i) < e) { + c = ca[s + i]; + if (isH(c)) { + if (nc > 0) { + nc--; + } else if (nv > 0) { + nv--; + } + } + } + } + return ((nc > 0) || (nv > 0)) ? s + i : -1; + } + } + } + + // gurmukhi character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // gurmukhi block range + static final int CCA_START = 0x0A00; // first code point mapped by cca + static final int CCA_END = 0x0A80; // last code point + 1 mapped by cca + // gurmukhi character type lookups + static final short[] CCA = { + C_U, // 0x0A00 // UNASSIGNED + C_O, // 0x0A01 // ADAK BINDI + C_O, // 0x0A02 // BINDI + C_O, // 0x0A03 // VISARGA + C_U, // 0x0A04 // UNASSIGNED + C_V, // 0x0A05 // A + C_V, // 0x0A06 // AA + C_V, // 0x0A07 // I + C_V, // 0x0A08 // II + C_V, // 0x0A09 // U + C_V, // 0x0A0A // UU + C_U, // 0x0A0B // UNASSIGNED + C_U, // 0x0A0C // UNASSIGNED + C_U, // 0x0A0D // UNASSIGNED + C_U, // 0x0A0E // UNASSIGNED + C_V, // 0x0A0F // E + C_V, // 0x0A10 // AI + C_U, // 0x0A11 // UNASSIGNED + C_U, // 0x0A12 // UNASSIGNED + C_V, // 0x0A13 // O + C_V, // 0x0A14 // AU + C_C, // 0x0A15 // KA + C_C, // 0x0A16 // KHA + C_C, // 0x0A17 // GA + C_C, // 0x0A18 // GHA + C_C, // 0x0A19 // NGA + C_C, // 0x0A1A // CA + C_C, // 0x0A1B // CHA + C_C, // 0x0A1C // JA + C_C, // 0x0A1D // JHA + C_C, // 0x0A1E // NYA + C_C, // 0x0A1F // TTA + C_C, // 0x0A20 // TTHA + C_C, // 0x0A21 // DDA + C_C, // 0x0A22 // DDHA + C_C, // 0x0A23 // NNA + C_C, // 0x0A24 // TA + C_C, // 0x0A25 // THA + C_C, // 0x0A26 // DA + C_C, // 0x0A27 // DHA + C_C, // 0x0A28 // NA + C_U, // 0x0A29 // UNASSIGNED + C_C, // 0x0A2A // PA + C_C, // 0x0A2B // PHA + C_C, // 0x0A2C // BA + C_C, // 0x0A2D // BHA + C_C, // 0x0A2E // MA + C_C, // 0x0A2F // YA + C_C | C_R, // 0x0A30 // RA + C_U, // 0x0A31 // UNASSIGNED + C_C, // 0x0A32 // LA + C_C, // 0x0A33 // LLA + C_U, // 0x0A34 // UNASSIGNED + C_C, // 0x0A35 // VA + C_C, // 0x0A36 // SHA + C_U, // 0x0A37 // UNASSIGNED + C_C, // 0x0A38 // SA + C_C, // 0x0A39 // HA + C_U, // 0x0A3A // UNASSIGNED + C_U, // 0x0A3B // UNASSIGNED + C_N, // 0x0A3C // NUKTA + C_U, // 0x0A3D // UNASSIGNED + C_M, // 0x0A3E // AA + C_M | C_PRE, // 0x0A3F // I + C_M, // 0x0A40 // II + C_M, // 0x0A41 // U + C_M, // 0x0A42 // UU + C_U, // 0x0A43 // UNASSIGNED + C_U, // 0x0A44 // UNASSIGNED + C_U, // 0x0A45 // UNASSIGNED + C_U, // 0x0A46 // UNASSIGNED + C_M, // 0x0A47 // EE + C_M, // 0x0A48 // AI + C_U, // 0x0A49 // UNASSIGNED + C_U, // 0x0A4A // UNASSIGNED + C_M, // 0x0A4B // OO + C_M, // 0x0A4C // AU + C_H, // 0x0A4D // VIRAMA (HALANT) + C_U, // 0x0A4E // UNASSIGNED + C_U, // 0x0A4F // UNASSIGNED + C_U, // 0x0A50 // UNASSIGNED + C_T, // 0x0A51 // UDATTA + C_U, // 0x0A52 // UNASSIGNED + C_U, // 0x0A53 // UNASSIGNED + C_U, // 0x0A54 // UNASSIGNED + C_U, // 0x0A55 // UNASSIGNED + C_U, // 0x0A56 // UNASSIGNED + C_U, // 0x0A57 // UNASSIGNED + C_U, // 0x0A58 // UNASSIGNED + C_C | C_N, // 0x0A59 // KHHA + C_C | C_N, // 0x0A5A // GHHA + C_C | C_N, // 0x0A5B // ZA + C_C | C_N, // 0x0A5C // RRA + C_U, // 0x0A5D // UNASSIGNED + C_C | C_N, // 0x0A5E // FA + C_U, // 0x0A5F // UNASSIGNED + C_U, // 0x0A60 // UNASSIGNED + C_U, // 0x0A61 // UNASSIGNED + C_U, // 0x0A62 // UNASSIGNED + C_U, // 0x0A63 // UNASSIGNED + C_U, // 0x0A64 // UNASSIGNED + C_U, // 0x0A65 // UNASSIGNED + C_D, // 0x0A66 // ZERO + C_D, // 0x0A67 // ONE + C_D, // 0x0A68 // TWO + C_D, // 0x0A69 // THREE + C_D, // 0x0A6A // FOUR + C_D, // 0x0A6B // FIVE + C_D, // 0x0A6C // SIX + C_D, // 0x0A6D // SEVEN + C_D, // 0x0A6E // EIGHT + C_D, // 0x0A6F // NINE + C_O, // 0x0A70 // TIPPI + C_O, // 0x0A71 // ADDAK + C_V, // 0x0A72 // IRI + C_V, // 0x0A73 // URA + C_S, // 0x0A74 // EK ONKAR + C_O, // 0x0A75 // YAKASH + C_U, // 0x0A76 // UNASSIGNED + C_U, // 0x0A77 // UNASSIGNED + C_U, // 0x0A78 // UNASSIGNED + C_U, // 0x0A79 // UNASSIGNED + C_U, // 0x0A7A // UNASSIGNED + C_U, // 0x0A7B // UNASSIGNED + C_U, // 0x0A7C // UNASSIGNED + C_U, // 0x0A7D // UNASSIGNED + C_U, // 0x0A7E // UNASSIGNED + C_U // 0x0A7F // UNASSIGNED + }; + + static int typeOf(int c) { + if ((c >= CCA_START) && (c < CCA_END)) { + return CCA[c - CCA_START] & C_M_TYPE; + } else { + return C_U; + } + } + + static boolean isType(int c, int t) { + return typeOf(c) == t; + } + + static boolean hasFlag(int c, int f) { + if ((c >= CCA_START) && (c < CCA_END)) { + return (CCA[c - CCA_START] & f) == f; + } else { + return false; + } + } + + static boolean isC(int c) { + return isType(c, C_C); + } + + static boolean isR(int c) { + return isType(c, C_C) && hasR(c); + } + + static boolean isV(int c) { + return isType(c, C_V); + } + + static boolean isN(int c) { + return c == 0x0A3C; + } + + static boolean isH(int c) { + return c == 0x0A4D; + } + + static boolean isM(int c) { + return isType(c, C_M); + } + + static boolean isPreM(int c) { + return isType(c, C_M) && hasFlag(c, C_PRE); + } + + static boolean isX(int c) { + switch (typeOf(c)) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + + static boolean hasR(int c) { + return hasFlag(c, C_R); + } + + static boolean hasN(int c) { + return hasFlag(c, C_N); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/IndicScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/IndicScriptProcessor.java new file mode 100644 index 0000000..950e589 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/IndicScriptProcessor.java @@ -0,0 +1,658 @@ +/* + * Copyright (C) 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. + * + */ + +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 + +/** + *

The IndicScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Indic script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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/**/ testerMap = new HashMap/**/(); + + public GlyphContextTester getTester(String feature) { + return (GlyphContextTester) testerMap.get(feature); + } + } + + private static class PositioningScriptContextTester implements ScriptContextTester { + + private static Map/**/ testerMap = new HashMap/**/(); + + 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 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 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(); + 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 presentationFeatures; + private static final String[] PRESENTATION_FEATURE_STRINGS = { + "abvs", + "blws", + "calt", + "haln", + "pres", + "psts", + }; + + static { + presentationFeatures = new HashSet(); + 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 syllabizers = new HashMap(); + + static Syllabizer getSyllabizer(String script, String language, Class 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 syllabizerClass) { + Syllabizer s; + try { + Constructor 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 sv = new Vector(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 nsv = new Vector(); + for (int i = 0, ns = sa.length; i < ns; i++) { + Segment s = sa[i]; + Vector ngv = new Vector(ng); + Vector nav = new Vector(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 iv) { + int ni = iv.size(); + int[] ia = new int[iv.size()]; + for (int i = 0, n = ni; i < n; i++) { + ia[i] = iv.get(i); + } + return ia; + } + } + + /** + * Syllabic segment. + */ + protected static class Segment { + + static final int OTHER = 0; // other (non-syllable) characters + static final int SYLLABLE = 1; // (orthographic) syllable + + private int start; + private int end; + private int type; + + Segment(int start, int end, int type) { + this.start = start; + this.end = end; + this.type = type; + } + + int getStart() { + return start; + } + + int getEnd() { + return end; + } + + int getOffset() { + return start; + } + + int getCount() { + return end - start; + } + + int getType() { + return type; + } + } +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ScriptProcessor.java new file mode 100644 index 0000000..9232f21 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/ScriptProcessor.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 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. + * + */ + +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; + +/** + *

Abstract script processor base class for which an implementation of the substitution and positioning methods + * must be supplied.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public abstract class ScriptProcessor { + + private final String script; + + private final Map assembledLookups; + + private static Map processors = new HashMap(); + + /** + * 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/*>>*/ 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/*>*/ 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/*>*/ 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/*>*/ lookups; + + AssembledLookupsKey(GlyphTable table, String[] features, Map/*>*/ lookups) { + this.table = table; + this.features = features; + this.lookups = lookups; + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + int hc = 0; + hc = 7 * hc + (hc ^ table.hashCode()); + hc = 11 * hc + (hc ^ Arrays.hashCode(features)); + hc = 17 * hc + (hc ^ lookups.hashCode()); + return hc; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object o) { + if (o instanceof AssembledLookupsKey) { + AssembledLookupsKey k = (AssembledLookupsKey) o; + if (!table.equals(k.table)) { + return false; + } else if (!Arrays.equals(features, k.features)) { + return false; + } else { + return lookups.equals(k.lookups); + } + } else { + return false; + } + } + + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/TamilScriptProcessor.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/TamilScriptProcessor.java new file mode 100644 index 0000000..ecb942c --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/scripts/TamilScriptProcessor.java @@ -0,0 +1,552 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.scripts; + +// CSOFF: LineLengthCheck + +import com.jaredrummler.fontreader.complexscripts.util.CharAssociation; +import com.jaredrummler.fontreader.util.GlyphSequence; + +/** + *

The TamilScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Tamil script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class TamilScriptProcessor extends IndicScriptProcessor { + + TamilScriptProcessor(String script) { + super(script); + } + + @Override + protected Class getSyllabizerClass() { + return TamilSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = ng; i > 0; i--) { + int k = i - 1; + if (containsPreBaseMatra(gs, k)) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = (source < ng) ? source : ng; i > 0; i--) { + int k = i - 1; + if (containsConsonant(gs, k)) { + if (containsHalfConsonant(gs, k)) { + lk = k; + } else if (lk == -1) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isPreM(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsConsonant(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isC(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant(GlyphSequence gs, int k) { + Boolean half = (Boolean) gs.getAssociation(k).getPredication("half"); + return (half != null) ? half.booleanValue() : false; + } + + @Override + protected int findReph(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int li = -1; + for (int i = 0; i < ng; i++) { + if (containsReph(gs, i)) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for (int i = 0; i < ng; i++) { + if ((i != source) && containsConsonant(gs, i)) { + if (!containsHalfConsonant(gs, i)) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) { + if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) { + c2 = i + 1; + } else if (containsOtherMark(gs, i)) { + c2 = i; + break; + } + } + if (c2 >= 0) { + return c2; + } else if (c1 >= 0) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph(GlyphSequence gs, int k) { + Boolean rphf = (Boolean) gs.getAssociation(k).getPredication("rphf"); + return (rphf != null) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isM(ca[i])) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + switch (typeOf(ca[i])) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class TamilSyllabizer extends DefaultSyllabizer { + + TamilSyllabizer(String script, String language) { + super(script, language); + } + + @Override + // | C ... + protected int findStartOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + while (s < e) { + int c = ca[s]; + if (isC(c)) { + break; + } else { + s++; + } + } + return s; + } + } + + @Override + // D* L? | ... + protected int findEndOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ((i = isDeadConsonant(ca, s, e)) > s) { + s = i; + nd++; + } + // consume zero or one live consonant + if ((i = isLiveConsonant(ca, s, e)) > s) { + s = i; + nl++; + } + return ((nd > 0) || (nl > 0)) ? s : -1; + } + } + + // D := ( C N? H )? + private int isDeadConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nh = 0; + do { + // C + if ((s + i) < e) { + c = ca[s + i]; + if (isC(c)) { + i++; + nc++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca[s + 1]; + if (isN(c)) { + i++; + } + } + // H + if ((s + i) < e) { + c = ca[s + i]; + if (isH(c)) { + i++; + nh++; + } else { + break; + } + } + } while (false); + return (nc > 0) && (nh > 0) ? s + i : -1; + } + } + + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nv = 0; + int nx = 0; + do { + // C + if ((s + i) < e) { + c = ca[s + i]; + if (isC(c)) { + i++; + nc++; + } else if (isV(c)) { + i++; + nv++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca[s + i]; + if (isN(c)) { + i++; + } + } + // X* + while ((s + i) < e) { + c = ca[s + i]; + if (isX(c)) { + i++; + nx++; + } else { + break; + } + } + } while (false); + // if no X but has H, then ignore C|I + if (nx == 0) { + if ((s + i) < e) { + c = ca[s + i]; + if (isH(c)) { + if (nc > 0) { + nc--; + } else if (nv > 0) { + nv--; + } + } + } + } + return ((nc > 0) || (nv > 0)) ? s + i : -1; + } + } + } + + // tamil character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_POST = 0x1000; // post-base + static final short C_WRAP = C_PRE | C_POST; // wrap (two part) vowel + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // tamil block range + static final int CCA_START = 0x0B80; // first code point mapped by cca + static final int CCA_END = 0x0C00; // last code point + 1 mapped by cca + // tamil character type lookups + static final short[] CCA = { + C_U, // 0x0B80 // + C_U, // 0x0B81 // + C_O, // 0x0B82 // ANUSVARA + C_O, // 0x0B83 // VISARGA + C_U, // 0x0B84 // + C_V, // 0x0B85 // A + C_V, // 0x0B86 // AA + C_V, // 0x0B87 // I + C_V, // 0x0B88 // II + C_V, // 0x0B89 // U + C_V, // 0x0B8A // UU + C_U, // 0x0B8B // + C_U, // 0x0B8C // + C_U, // 0x0B8D // + C_V, // 0x0B8E // E + C_V, // 0x0B8F // EE + C_V, // 0x0B90 // AI + C_U, // 0x0B91 // + C_V, // 0x0B92 // O + C_V, // 0x0B93 // OO + C_V, // 0x0B94 // AU + C_C, // 0x0B95 // KA + C_U, // 0x0B96 // + C_U, // 0x0B97 // + C_U, // 0x0B98 // + C_C, // 0x0B99 // NGA + C_C, // 0x0B9A // CA + C_U, // 0x0B9B // + C_C, // 0x0B9C // JA + C_U, // 0x0B9D // + C_C, // 0x0B9E // NYA + C_C, // 0x0B9F // TTA + C_U, // 0x0BA0 // + C_U, // 0x0BA1 // + C_U, // 0x0BA2 // + C_C, // 0x0BA3 // NNA + C_C, // 0x0BA4 // TA + C_U, // 0x0BA5 // + C_U, // 0x0BA6 // + C_U, // 0x0BA7 // + C_C, // 0x0BA8 // NA + C_C, // 0x0BA9 // NNNA + C_C, // 0x0BAA // PA + C_U, // 0x0BAB // + C_U, // 0x0BAC // + C_U, // 0x0BAD // + C_C, // 0x0BAE // MA + C_C, // 0x0BAF // YA + C_C | C_R, // 0x0BB0 // RA + C_C | C_R, // 0x0BB1 // RRA + C_C, // 0x0BB2 // LA + C_C, // 0x0BB3 // LLA + C_C, // 0x0BB4 // LLLA + C_C, // 0x0BB5 // VA + C_C, // 0x0BB6 // SHA + C_C, // 0x0BB7 // SSA + C_C, // 0x0BB8 // SA + C_C, // 0x0BB9 // HA + C_U, // 0x0BBA // + C_U, // 0x0BBB // + C_U, // 0x0BBC // + C_U, // 0x0BBD // + C_M, // 0x0BBE // AA + C_M, // 0x0BBF // I + C_M, // 0x0BC0 // II + C_M, // 0x0BC1 // U + C_M, // 0x0BC2 // UU + C_U, // 0x0BC3 // + C_U, // 0x0BC4 // + C_U, // 0x0BC5 // + C_M | C_PRE, // 0x0BC6 // E + C_M | C_PRE, // 0x0BC7 // EE + C_M | C_PRE, // 0x0BC8 // AI + C_U, // 0x0BC9 // + C_M | C_WRAP, // 0x0BCA // O + C_M | C_WRAP, // 0x0BCB // OO + C_M | C_WRAP, // 0x0BCC // AU + C_H, // 0x0BCD // VIRAMA (HALANT) + C_U, // 0x0BCE // + C_U, // 0x0BCF // + C_S, // 0x0BD0 // OM + C_U, // 0x0BD1 // + C_U, // 0x0BD2 // + C_U, // 0x0BD3 // + C_U, // 0x0BD4 // + C_U, // 0x0BD5 // + C_U, // 0x0BD6 // + C_M, // 0x0BD7 // AU LENGTH MARK + C_U, // 0x0BD8 // + C_U, // 0x0BD9 // + C_U, // 0x0BDA // + C_U, // 0x0BDB // + C_U, // 0x0BDC // + C_U, // 0x0BDD // + C_U, // 0x0BDE // + C_U, // 0x0BDF // + C_U, // 0x0BE0 // + C_U, // 0x0BE1 // + C_U, // 0x0BE2 // + C_U, // 0x0BE3 // + C_U, // 0x0BE4 // + C_U, // 0x0BE5 // + C_D, // 0x0BE6 // ZERO + C_D, // 0x0BE7 // ONE + C_D, // 0x0BE8 // TWO + C_D, // 0x0BE9 // THREE + C_D, // 0x0BEA // FOUR + C_D, // 0x0BEB // FIVE + C_D, // 0x0BEC // SIX + C_D, // 0x0BED // SEVEN + C_D, // 0x0BEE // EIGHT + C_D, // 0x0BEF // NINE + C_S, // 0x0BF0 // TEN + C_S, // 0x0BF1 // ONE HUNDRED + C_S, // 0x0BF2 // ONE THOUSAND + C_S, // 0x0BF3 // DAY SIGN (naal) + C_S, // 0x0BF4 // MONTH SIGN (maatham) + C_S, // 0x0BF5 // YEAR SIGN (varudam) + C_S, // 0x0BF6 // DEBIT SIGN (patru) + C_S, // 0x0BF7 // CREDIT SIGN (varavu) + C_S, // 0x0BF8 // AS ABOVE SIGN (merpadi) + C_S, // 0x0BF9 // RUPEE SIGN (rupai) + C_S, // 0x0BFA // NUMBER SIGN (enn) + C_U, // 0x0BFB // + C_U, // 0x0BFC // + C_U, // 0x0BFD // + C_U, // 0x0BFE // + C_U // 0x0BFF // + }; + + static int typeOf(int c) { + if ((c >= CCA_START) && (c < CCA_END)) { + return CCA[c - CCA_START] & C_M_TYPE; + } else { + return C_U; + } + } + + static boolean isType(int c, int t) { + return typeOf(c) == t; + } + + static boolean hasFlag(int c, int f) { + if ((c >= CCA_START) && (c < CCA_END)) { + return (CCA[c - CCA_START] & f) == f; + } else { + return false; + } + } + + static boolean isC(int c) { + return isType(c, C_C); + } + + static boolean isR(int c) { + return isType(c, C_C) && hasR(c); + } + + static boolean isV(int c) { + return isType(c, C_V); + } + + static boolean isN(int c) { + return c == 0x093C; + } + + static boolean isH(int c) { + return c == 0x094D; + } + + static boolean isM(int c) { + return isType(c, C_M); + } + + static boolean isPreM(int c) { + return isType(c, C_M) && hasFlag(c, C_PRE); + } + + static boolean isX(int c) { + switch (typeOf(c)) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + + static boolean hasR(int c) { + return hasFlag(c, C_R); + } + + static boolean hasN(int c) { + return hasFlag(c, C_N); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharAssociation.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharAssociation.java new file mode 100644 index 0000000..1047fe0 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharAssociation.java @@ -0,0 +1,517 @@ +/* + * Copyright (C) 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. + * + */ + +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 CharAssociation is used to maintain a + * backpointer from a glyph to one or more character intervals from which the glyph was derived. + *

+ * Each glyph in a glyph sequence is associated with a single CharAssociation instance. + *

+ * A CharAssociation 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. + * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class CharAssociation implements Cloneable { + + // instance state + private final int offset; + private final int count; + private final int[] subIntervals; + private Map predications; + + // class state + private static volatile Map 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 . + * + * @param key predication key + * @param value predication value + */ + public void setPredication(String key, Object value) { + if (predications == null) { + predications = new HashMap(); + } + 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 . + * + * @param key predication key + * @param value predication value + */ + public void mergePredication(String key, Object value) { + if (predications == null) { + predications = new HashMap(); + } + 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 PredicationMerger + * 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 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(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(); + } + 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 repeat new associations. + * + * @param a association to replicate + * @param repeat count + * @return array of replicated associations + */ + public static CharAssociation[] replicate(CharAssociation a, int repeat) { + CharAssociation[] aa = new CharAssociation[repeat]; + for (int i = 0, n = aa.length; i < n; i++) { + aa[i] = (CharAssociation) a.clone(); + } + return aa; + } + + /** + * Join (merge) multiple associations into a single, potentially disjoint + * association. + * + * @param aa array of associations to join + * @return (possibly disjoint) association containing joined associations + */ + public static CharAssociation join(CharAssociation[] aa) { + CharAssociation ca; + // extract sorted intervals + int[] ia = extractIntervals(aa); + if ((ia == null) || (ia.length == 0)) { + ca = new CharAssociation(0, 0); + } else if (ia.length == 2) { + int s = ia[0]; + int e = ia[1]; + ca = new CharAssociation(s, e - s); + } else { + ca = new CharAssociation(mergeIntervals(ia)); + } + return mergePredicates(ca, aa); + } + + private static CharAssociation mergePredicates(CharAssociation ca, CharAssociation[] aa) { + for (CharAssociation a : aa) { + ca.mergePredications(a); + } + return ca; + } + + private static int getSubIntervalsStart(int[] ia) { + int us = Integer.MAX_VALUE; + int ue = Integer.MIN_VALUE; + if (ia != null) { + for (int i = 0, n = ia.length; i < n; i += 2) { + int s = ia[i + 0]; + int e = ia[i + 1]; + if (s < us) { + us = s; + } + if (e > ue) { + ue = e; + } + } + if (ue < 0) { + ue = 0; + } + if (us > ue) { + us = ue; + } + } + return us; + } + + private static int getSubIntervalsLength(int[] ia) { + int us = Integer.MAX_VALUE; + int ue = Integer.MIN_VALUE; + if (ia != null) { + for (int i = 0, n = ia.length; i < n; i += 2) { + int s = ia[i + 0]; + int e = ia[i + 1]; + if (s < us) { + us = s; + } + if (e > ue) { + ue = e; + } + } + if (ue < 0) { + ue = 0; + } + if (us > ue) { + us = ue; + } + } + return ue - us; + } + + /** + * Extract sorted sub-intervals. + */ + private static int[] extractIntervals(CharAssociation[] aa) { + int ni = 0; + for (int i = 0, n = aa.length; i < n; i++) { + CharAssociation a = aa[i]; + if (a.isDisjoint()) { + ni += a.getSubIntervalCount(); + } else { + ni += 1; + } + } + int[] sa = new int[ni]; + int[] ea = new int[ni]; + for (int i = 0, k = 0; i < aa.length; i++) { + CharAssociation a = aa[i]; + if (a.isDisjoint()) { + int[] da = a.getSubIntervals(); + for (int j = 0; j < da.length; j += 2) { + sa[k] = da[j + 0]; + ea[k] = da[j + 1]; + k++; + } + } else { + sa[k] = a.getStart(); + ea[k] = a.getEnd(); + k++; + } + } + return sortIntervals(sa, ea); + } + + private static final int[] SORT_INCREMENTS_16 + = {1391376, 463792, 198768, 86961, 33936, 13776, 4592, 1968, 861, 336, 112, 48, 21, 7, 3, 1}; + + private static final int[] SORT_INCREMENTS_03 + = {7, 3, 1}; + + /** + * Sort sub-intervals using modified Shell Sort. + */ + private static int[] sortIntervals(int[] sa, int[] ea) { + assert sa != null; + assert ea != null; + assert sa.length == ea.length; + int ni = sa.length; + int[] incr = (ni < 21) ? SORT_INCREMENTS_03 : SORT_INCREMENTS_16; + for (int k = 0; k < incr.length; k++) { + for (int h = incr[k], i = h, n = ni, j; i < n; i++) { + int s1 = sa[i]; + int e1 = ea[i]; + for (j = i; j >= h; j -= h) { + int s2 = sa[j - h]; + int e2 = ea[j - h]; + if (s2 > s1) { + sa[j] = s2; + ea[j] = e2; + } else if ((s2 == s1) && (e2 > e1)) { + sa[j] = s2; + ea[j] = e2; + } else { + break; + } + } + sa[j] = s1; + ea[j] = e1; + } + } + int[] ia = new int[ni * 2]; + for (int i = 0; i < ni; i++) { + ia[(i * 2) + 0] = sa[i]; + ia[(i * 2) + 1] = ea[i]; + } + return ia; + } + + /** + * Merge overlapping and abutting sub-intervals. + */ + private static int[] mergeIntervals(int[] ia) { + int ni = ia.length; + int i; + int n; + int nm; + int is; + int ie; + // count merged sub-intervals + for (i = 0, n = ni, nm = 0, is = ie = -1; i < n; i += 2) { + int s = ia[i + 0]; + int e = ia[i + 1]; + if ((ie < 0) || (s > ie)) { + is = s; + ie = e; + nm++; + } else if (s >= is) { + if (e > ie) { + ie = e; + } + } + } + int[] mi = new int[nm * 2]; + // populate merged sub-intervals + for (i = 0, n = ni, nm = 0, is = ie = -1; i < n; i += 2) { + int s = ia[i + 0]; + int e = ia[i + 1]; + int k = nm * 2; + if ((ie < 0) || (s > ie)) { + is = s; + ie = e; + mi[k + 0] = is; + mi[k + 1] = ie; + nm++; + } else if (s >= is) { + if (e > ie) { + ie = e; + } + mi[k - 1] = ie; + } + } + return mi; + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append('['); + sb.append(offset); + sb.append(','); + sb.append(count); + sb.append(']'); + return sb.toString(); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharScript.java b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharScript.java new file mode 100644 index 0000000..4994be8 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/complexscripts/util/CharScript.java @@ -0,0 +1,955 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.complexscripts.util; + +import com.jaredrummler.fontreader.util.CharUtilities; + +import java.util.*; + +/** + *

Script related utilities.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 true. + */ + 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 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 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 scriptTagsMap; + private static Map 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 tm = new HashMap(); + HashMap cm = new HashMap(); + 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 getScriptTagsMap() { + if (scriptTagsMap == null) { + makeScriptMaps(); + } + return scriptTagsMap; + } + + private static Map getScriptCodeMap() { + if (scriptCodeMap == null) { + makeScriptMaps(); + } + return scriptCodeMap; + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CMapSegment.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CMapSegment.java new file mode 100644 index 0000000..177f56a --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CMapSegment.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.fonts; + +/** + * A segment in a cmap table of format 4. Unicode code points between + * {@link #getUnicodeStart()} and {@link #getUnicodeEnd()} map to contiguous glyph indices + * starting from {@link #getGlyphStartIndex()}. + */ +public final class CMapSegment { + + private final int unicodeStart; + private final int unicodeEnd; + private final int glyphStartIndex; + + /** + * Creates a new segment. + * + * @param unicodeStart Unicode start index + * @param unicodeEnd Unicode end index + * @param glyphStartIndex glyph start index + */ + public CMapSegment(int unicodeStart, int unicodeEnd, int glyphStartIndex) { + this.unicodeStart = unicodeStart; + this.unicodeEnd = unicodeEnd; + this.glyphStartIndex = glyphStartIndex; + } + + @Override + public int hashCode() { + int hc = 17; + hc = 31 * hc + unicodeStart; + hc = 31 * hc + unicodeEnd; + hc = 31 * hc + glyphStartIndex; + return hc; + } + + @Override + public boolean equals(Object o) { + if (o instanceof CMapSegment) { + CMapSegment ce = (CMapSegment) o; + return ce.unicodeStart == this.unicodeStart + && ce.unicodeEnd == this.unicodeEnd + && ce.glyphStartIndex == this.glyphStartIndex; + } + return false; + } + + /** + * Returns the unicodeStart. + * + * @return the Unicode start index + */ + public int getUnicodeStart() { + return unicodeStart; + } + + /** + * Returns the unicodeEnd. + * + * @return the Unicode end index + */ + public int getUnicodeEnd() { + return unicodeEnd; + } + + /** + * Returns the glyphStartIndex. + * + * @return the glyph start index + */ + public int getGlyphStartIndex() { + return glyphStartIndex; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("CMapSegment: "); + sb.append("{ UC["); + sb.append(unicodeStart); + sb.append(','); + sb.append(unicodeEnd); + sb.append("]: GC["); + sb.append(glyphStartIndex); + sb.append(','); + sb.append(glyphStartIndex + (unicodeEnd - unicodeStart)); + sb.append("] }"); + return sb.toString(); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CodePointMapping.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CodePointMapping.java new file mode 100644 index 0000000..40e20f1 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/CodePointMapping.java @@ -0,0 +1,1804 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.fonts; + +import java.util.Collections; +import java.util.Map; + +public class CodePointMapping { + + private char[] latin1Map; + private char[] characters; + private char[] codepoints; + + private CodePointMapping(int[] table) { + int nonLatin1 = 0; + latin1Map = new char[256]; + for (int i = 0; i < table.length; i += 2) { + if (table[i + 1] < 256) + latin1Map[table[i + 1]] = (char) table[i]; + else + ++nonLatin1; + } + characters = new char[nonLatin1]; + codepoints = new char[nonLatin1]; + int top = 0; + for (int i = 0; i < table.length; i += 2) { + char c = (char) table[i + 1]; + if (c >= 256) { + ++top; + for (int j = top - 1; j >= 0; --j) { + if (j > 0 && characters[j - 1] >= c) { + characters[j] = characters[j - 1]; + codepoints[j] = codepoints[j - 1]; + } else { + characters[j] = c; + codepoints[j] = (char) table[i]; + break; + } + } + } + } + } + + public final char mapChar(char c) { + if (c < 256) { + return latin1Map[c]; + } else { + int bot = 0, top = characters.length - 1; + while (top >= bot) { + int mid = (bot + top) / 2; + char mc = characters[mid]; + + if (c == mc) + return codepoints[mid]; + else if (c < mc) + top = mid - 1; + else + bot = mid + 1; + } + return 0; + } + } + + private static Map mappings; + + static { + mappings = Collections.synchronizedMap(new java.util.HashMap()); + } + + public static CodePointMapping getMapping(String encoding) { + CodePointMapping mapping = (CodePointMapping) mappings.get(encoding); + if (mapping != null) { + return mapping; + } else if (encoding.equals("StandardEncoding")) { + mapping = new CodePointMapping(encStandardEncoding); + mappings.put("StandardEncoding", mapping); + return mapping; + } else if (encoding.equals("ISOLatin1Encoding")) { + mapping = new CodePointMapping(encISOLatin1Encoding); + mappings.put("ISOLatin1Encoding", mapping); + return mapping; + } else if (encoding.equals("CEEncoding")) { + mapping = new CodePointMapping(encCEEncoding); + mappings.put("CEEncoding", mapping); + return mapping; + } else if (encoding.equals("MacRomanEncoding")) { + mapping = new CodePointMapping(encMacRomanEncoding); + mappings.put("MacRomanEncoding", mapping); + return mapping; + } else if (encoding.equals("WinAnsiEncoding")) { + mapping = new CodePointMapping(encWinAnsiEncoding); + mappings.put("WinAnsiEncoding", mapping); + return mapping; + } else if (encoding.equals("PDFDocEncoding")) { + mapping = new CodePointMapping(encPDFDocEncoding); + mappings.put("PDFDocEncoding", mapping); + return mapping; + } else if (encoding.equals("SymbolEncoding")) { + mapping = new CodePointMapping(encSymbolEncoding); + mappings.put("SymbolEncoding", mapping); + return mapping; + } else if (encoding.equals("ZapfDingbatsEncoding")) { + mapping = new CodePointMapping(encZapfDingbatsEncoding); + mappings.put("ZapfDingbatsEncoding", mapping); + return mapping; + } else { + return null; + } + } + + private static final int[] encStandardEncoding + = { + 0x20, 0x0020, // space + 0x20, 0x00A0, // space + 0x21, 0x0021, // exclam + 0x22, 0x0022, // quotedbl + 0x23, 0x0023, // numbersign + 0x24, 0x0024, // dollar + 0x25, 0x0025, // percent + 0x26, 0x0026, // ampersand + 0x27, 0x2019, // quoteright + 0x28, 0x0028, // parenleft + 0x29, 0x0029, // parenright + 0x2a, 0x002A, // asterisk + 0x2b, 0x002B, // plus + 0x2c, 0x002C, // comma + 0x2d, 0x002D, // hyphen + 0x2d, 0x00AD, // hyphen + 0x2e, 0x002E, // period + 0x2f, 0x002F, // slash + 0x30, 0x0030, // zero + 0x31, 0x0031, // one + 0x32, 0x0032, // two + 0x33, 0x0033, // three + 0x34, 0x0034, // four + 0x35, 0x0035, // five + 0x36, 0x0036, // six + 0x37, 0x0037, // seven + 0x38, 0x0038, // eight + 0x39, 0x0039, // nine + 0x3a, 0x003A, // colon + 0x3b, 0x003B, // semicolon + 0x3c, 0x003C, // less + 0x3d, 0x003D, // equal + 0x3e, 0x003E, // greater + 0x3f, 0x003F, // question + 0x40, 0x0040, // at + 0x41, 0x0041, // A + 0x42, 0x0042, // B + 0x43, 0x0043, // C + 0x44, 0x0044, // D + 0x45, 0x0045, // E + 0x46, 0x0046, // F + 0x47, 0x0047, // G + 0x48, 0x0048, // H + 0x49, 0x0049, // I + 0x4a, 0x004A, // J + 0x4b, 0x004B, // K + 0x4c, 0x004C, // L + 0x4d, 0x004D, // M + 0x4e, 0x004E, // N + 0x4f, 0x004F, // O + 0x50, 0x0050, // P + 0x51, 0x0051, // Q + 0x52, 0x0052, // R + 0x53, 0x0053, // S + 0x54, 0x0054, // T + 0x55, 0x0055, // U + 0x56, 0x0056, // V + 0x57, 0x0057, // W + 0x58, 0x0058, // X + 0x59, 0x0059, // Y + 0x5a, 0x005A, // Z + 0x5b, 0x005B, // bracketleft + 0x5c, 0x005C, // backslash + 0x5d, 0x005D, // bracketright + 0x5e, 0x005E, // asciicircum + 0x5f, 0x005F, // underscore + 0x60, 0x2018, // quoteleft + 0x61, 0x0061, // a + 0x62, 0x0062, // b + 0x63, 0x0063, // c + 0x64, 0x0064, // d + 0x65, 0x0065, // e + 0x66, 0x0066, // f + 0x67, 0x0067, // g + 0x68, 0x0068, // h + 0x69, 0x0069, // i + 0x6a, 0x006A, // j + 0x6b, 0x006B, // k + 0x6c, 0x006C, // l + 0x6d, 0x006D, // m + 0x6e, 0x006E, // n + 0x6f, 0x006F, // o + 0x70, 0x0070, // p + 0x71, 0x0071, // q + 0x72, 0x0072, // r + 0x73, 0x0073, // s + 0x74, 0x0074, // t + 0x75, 0x0075, // u + 0x76, 0x0076, // v + 0x77, 0x0077, // w + 0x78, 0x0078, // x + 0x79, 0x0079, // y + 0x7a, 0x007A, // z + 0x7b, 0x007B, // braceleft + 0x7c, 0x007C, // bar + 0x7d, 0x007D, // braceright + 0x7e, 0x007E, // asciitilde + 0xa1, 0x00A1, // exclamdown + 0xa2, 0x00A2, // cent + 0xa3, 0x00A3, // sterling + 0xa4, 0x2044, // fraction + 0xa4, 0x2215, // fraction + 0xa5, 0x00A5, // yen + 0xa6, 0x0192, // florin + 0xa7, 0x00A7, // section + 0xa8, 0x00A4, // currency + 0xa9, 0x0027, // quotesingle + 0xaa, 0x201C, // quotedblleft + 0xab, 0x00AB, // guillemotleft + 0xac, 0x2039, // guilsinglleft + 0xad, 0x203A, // guilsinglright + 0xae, 0xFB01, // fi + 0xaf, 0xFB02, // fl + 0xb1, 0x2013, // endash + 0xb2, 0x2020, // dagger + 0xb3, 0x2021, // daggerdbl + 0xb4, 0x00B7, // periodcentered + 0xb4, 0x2219, // periodcentered + 0xb6, 0x00B6, // paragraph + 0xb7, 0x2022, // bullet + 0xb8, 0x201A, // quotesinglbase + 0xb9, 0x201E, // quotedblbase + 0xba, 0x201D, // quotedblright + 0xbb, 0x00BB, // guillemotright + 0xbc, 0x2026, // ellipsis + 0xbd, 0x2030, // perthousand + 0xbf, 0x00BF, // questiondown + 0xc1, 0x0060, // grave + 0xc2, 0x00B4, // acute + 0xc3, 0x02C6, // circumflex + 0xc4, 0x02DC, // tilde + 0xc5, 0x00AF, // macron + 0xc5, 0x02C9, // macron + 0xc6, 0x02D8, // breve + 0xc7, 0x02D9, // dotaccent + 0xc8, 0x00A8, // dieresis + 0xca, 0x02DA, // ring + 0xcb, 0x00B8, // cedilla + 0xcd, 0x02DD, // hungarumlaut + 0xce, 0x02DB, // ogonek + 0xcf, 0x02C7, // caron + 0xd0, 0x2014, // emdash + 0xe1, 0x00C6, // AE + 0xe3, 0x00AA, // ordfeminine + 0xe8, 0x0141, // Lslash + 0xe9, 0x00D8, // Oslash + 0xea, 0x0152, // OE + 0xeb, 0x00BA, // ordmasculine + 0xf1, 0x00E6, // ae + 0xf5, 0x0131, // dotlessi + 0xf8, 0x0142, // lslash + 0xf9, 0x00F8, // oslash + 0xfa, 0x0153, // oe + 0xfb, 0x00DF, // germandbls + }; + + private static final int[] encISOLatin1Encoding + = { + 0x20, 0x0020, // space + 0x20, 0x00A0, // space + 0x21, 0x0021, // exclam + 0x22, 0x0022, // quotedbl + 0x23, 0x0023, // numbersign + 0x24, 0x0024, // dollar + 0x25, 0x0025, // percent + 0x26, 0x0026, // ampersand + 0x27, 0x2019, // quoteright + 0x28, 0x0028, // parenleft + 0x29, 0x0029, // parenright + 0x2a, 0x002A, // asterisk + 0x2b, 0x002B, // plus + 0x2c, 0x002C, // comma + 0x2d, 0x2212, // minus + 0x2e, 0x002E, // period + 0x2f, 0x002F, // slash + 0x30, 0x0030, // zero + 0x31, 0x0031, // one + 0x32, 0x0032, // two + 0x33, 0x0033, // three + 0x34, 0x0034, // four + 0x35, 0x0035, // five + 0x36, 0x0036, // six + 0x37, 0x0037, // seven + 0x38, 0x0038, // eight + 0x39, 0x0039, // nine + 0x3a, 0x003A, // colon + 0x3b, 0x003B, // semicolon + 0x3c, 0x003C, // less + 0x3d, 0x003D, // equal + 0x3e, 0x003E, // greater + 0x3f, 0x003F, // question + 0x40, 0x0040, // at + 0x41, 0x0041, // A + 0x42, 0x0042, // B + 0x43, 0x0043, // C + 0x44, 0x0044, // D + 0x45, 0x0045, // E + 0x46, 0x0046, // F + 0x47, 0x0047, // G + 0x48, 0x0048, // H + 0x49, 0x0049, // I + 0x4a, 0x004A, // J + 0x4b, 0x004B, // K + 0x4c, 0x004C, // L + 0x4d, 0x004D, // M + 0x4e, 0x004E, // N + 0x4f, 0x004F, // O + 0x50, 0x0050, // P + 0x51, 0x0051, // Q + 0x52, 0x0052, // R + 0x53, 0x0053, // S + 0x54, 0x0054, // T + 0x55, 0x0055, // U + 0x56, 0x0056, // V + 0x57, 0x0057, // W + 0x58, 0x0058, // X + 0x59, 0x0059, // Y + 0x5a, 0x005A, // Z + 0x5b, 0x005B, // bracketleft + 0x5c, 0x005C, // backslash + 0x5d, 0x005D, // bracketright + 0x5e, 0x005E, // asciicircum + 0x5f, 0x005F, // underscore + 0x60, 0x2018, // quoteleft + 0x61, 0x0061, // a + 0x62, 0x0062, // b + 0x63, 0x0063, // c + 0x64, 0x0064, // d + 0x65, 0x0065, // e + 0x66, 0x0066, // f + 0x67, 0x0067, // g + 0x68, 0x0068, // h + 0x69, 0x0069, // i + 0x6a, 0x006A, // j + 0x6b, 0x006B, // k + 0x6c, 0x006C, // l + 0x6d, 0x006D, // m + 0x6e, 0x006E, // n + 0x6f, 0x006F, // o + 0x70, 0x0070, // p + 0x71, 0x0071, // q + 0x72, 0x0072, // r + 0x73, 0x0073, // s + 0x74, 0x0074, // t + 0x75, 0x0075, // u + 0x76, 0x0076, // v + 0x77, 0x0077, // w + 0x78, 0x0078, // x + 0x79, 0x0079, // y + 0x7a, 0x007A, // z + 0x7b, 0x007B, // braceleft + 0x7c, 0x007C, // bar + 0x7d, 0x007D, // braceright + 0x7e, 0x007E, // asciitilde + 0x90, 0x0131, // dotlessi + 0x91, 0x0060, // grave + 0x93, 0x02C6, // circumflex + 0x94, 0x02DC, // tilde + 0x96, 0x02D8, // breve + 0x97, 0x02D9, // dotaccent + 0x9a, 0x02DA, // ring + 0x9d, 0x02DD, // hungarumlaut + 0x9e, 0x02DB, // ogonek + 0x9f, 0x02C7, // caron + 0xa1, 0x00A1, // exclamdown + 0xa2, 0x00A2, // cent + 0xa3, 0x00A3, // sterling + 0xa4, 0x00A4, // currency + 0xa5, 0x00A5, // yen + 0xa6, 0x00A6, // brokenbar + 0xa7, 0x00A7, // section + 0xa8, 0x00A8, // dieresis + 0xa9, 0x00A9, // copyright + 0xaa, 0x00AA, // ordfeminine + 0xab, 0x00AB, // guillemotleft + 0xac, 0x00AC, // logicalnot + 0xad, 0x002D, // hyphen + 0xad, 0x00AD, // hyphen + 0xae, 0x00AE, // registered + 0xaf, 0x00AF, // macron + 0xaf, 0x02C9, // macron + 0xb0, 0x00B0, // degree + 0xb1, 0x00B1, // plusminus + 0xb2, 0x00B2, // twosuperior + 0xb3, 0x00B3, // threesuperior + 0xb4, 0x00B4, // acute + 0xb5, 0x00B5, // mu + 0xb5, 0x03BC, // mu + 0xb6, 0x00B6, // paragraph + 0xb7, 0x00B7, // periodcentered + 0xb7, 0x2219, // periodcentered + 0xb8, 0x00B8, // cedilla + 0xb9, 0x00B9, // onesuperior + 0xba, 0x00BA, // ordmasculine + 0xbb, 0x00BB, // guillemotright + 0xbc, 0x00BC, // onequarter + 0xbd, 0x00BD, // onehalf + 0xbe, 0x00BE, // threequarters + 0xbf, 0x00BF, // questiondown + 0xc0, 0x00C0, // Agrave + 0xc1, 0x00C1, // Aacute + 0xc2, 0x00C2, // Acircumflex + 0xc3, 0x00C3, // Atilde + 0xc4, 0x00C4, // Adieresis + 0xc5, 0x00C5, // Aring + 0xc6, 0x00C6, // AE + 0xc7, 0x00C7, // Ccedilla + 0xc8, 0x00C8, // Egrave + 0xc9, 0x00C9, // Eacute + 0xca, 0x00CA, // Ecircumflex + 0xcb, 0x00CB, // Edieresis + 0xcc, 0x00CC, // Igrave + 0xcd, 0x00CD, // Iacute + 0xce, 0x00CE, // Icircumflex + 0xcf, 0x00CF, // Idieresis + 0xd0, 0x00D0, // Eth + 0xd1, 0x00D1, // Ntilde + 0xd2, 0x00D2, // Ograve + 0xd3, 0x00D3, // Oacute + 0xd4, 0x00D4, // Ocircumflex + 0xd5, 0x00D5, // Otilde + 0xd6, 0x00D6, // Odieresis + 0xd7, 0x00D7, // multiply + 0xd8, 0x00D8, // Oslash + 0xd9, 0x00D9, // Ugrave + 0xda, 0x00DA, // Uacute + 0xdb, 0x00DB, // Ucircumflex + 0xdc, 0x00DC, // Udieresis + 0xdd, 0x00DD, // Yacute + 0xde, 0x00DE, // Thorn + 0xdf, 0x00DF, // germandbls + 0xe0, 0x00E0, // agrave + 0xe1, 0x00E1, // aacute + 0xe2, 0x00E2, // acircumflex + 0xe3, 0x00E3, // atilde + 0xe4, 0x00E4, // adieresis + 0xe5, 0x00E5, // aring + 0xe6, 0x00E6, // ae + 0xe7, 0x00E7, // ccedilla + 0xe8, 0x00E8, // egrave + 0xe9, 0x00E9, // eacute + 0xea, 0x00EA, // ecircumflex + 0xeb, 0x00EB, // edieresis + 0xec, 0x00EC, // igrave + 0xed, 0x00ED, // iacute + 0xee, 0x00EE, // icircumflex + 0xef, 0x00EF, // idieresis + 0xf0, 0x00F0, // eth + 0xf1, 0x00F1, // ntilde + 0xf2, 0x00F2, // ograve + 0xf3, 0x00F3, // oacute + 0xf4, 0x00F4, // ocircumflex + 0xf5, 0x00F5, // otilde + 0xf6, 0x00F6, // odieresis + 0xf7, 0x00F7, // divide + 0xf8, 0x00F8, // oslash + 0xf9, 0x00F9, // ugrave + 0xfa, 0x00FA, // uacute + 0xfb, 0x00FB, // ucircumflex + 0xfc, 0x00FC, // udieresis + 0xfd, 0x00FD, // yacute + 0xfe, 0x00FE, // thorn + 0xff, 0x00FF, // ydieresis + }; + + private static final int[] encCEEncoding + = { + 0x20, 0x0020, // space + 0x20, 0x00A0, // space + 0x21, 0x0021, // exclam + 0x22, 0x0022, // quotedbl + 0x23, 0x0023, // numbersign + 0x24, 0x0024, // dollar + 0x25, 0x0025, // percent + 0x26, 0x0026, // ampersand + 0x27, 0x0027, // quotesingle + 0x28, 0x0028, // parenleft + 0x29, 0x0029, // parenright + 0x2a, 0x002A, // asterisk + 0x2b, 0x002B, // plus + 0x2c, 0x002C, // comma + 0x2d, 0x002D, // hyphen + 0x2d, 0x00AD, // hyphen + 0x2e, 0x002E, // period + 0x2f, 0x002F, // slash + 0x30, 0x0030, // zero + 0x31, 0x0031, // one + 0x32, 0x0032, // two + 0x33, 0x0033, // three + 0x34, 0x0034, // four + 0x35, 0x0035, // five + 0x36, 0x0036, // six + 0x37, 0x0037, // seven + 0x38, 0x0038, // eight + 0x39, 0x0039, // nine + 0x3a, 0x003A, // colon + 0x3b, 0x003B, // semicolon + 0x3c, 0x003C, // less + 0x3d, 0x003D, // equal + 0x3e, 0x003E, // greater + 0x3f, 0x003F, // question + 0x40, 0x0040, // at + 0x41, 0x0041, // A + 0x42, 0x0042, // B + 0x43, 0x0043, // C + 0x44, 0x0044, // D + 0x45, 0x0045, // E + 0x46, 0x0046, // F + 0x47, 0x0047, // G + 0x48, 0x0048, // H + 0x49, 0x0049, // I + 0x4a, 0x004A, // J + 0x4b, 0x004B, // K + 0x4c, 0x004C, // L + 0x4d, 0x004D, // M + 0x4e, 0x004E, // N + 0x4f, 0x004F, // O + 0x50, 0x0050, // P + 0x51, 0x0051, // Q + 0x52, 0x0052, // R + 0x53, 0x0053, // S + 0x54, 0x0054, // T + 0x55, 0x0055, // U + 0x56, 0x0056, // V + 0x57, 0x0057, // W + 0x58, 0x0058, // X + 0x59, 0x0059, // Y + 0x5a, 0x005A, // Z + 0x5b, 0x005B, // bracketleft + 0x5c, 0x005C, // backslash + 0x5d, 0x005D, // bracketright + 0x5e, 0x005E, // asciicircum + 0x5f, 0x005F, // underscore + 0x60, 0x0060, // grave + 0x61, 0x0061, // a + 0x62, 0x0062, // b + 0x63, 0x0063, // c + 0x64, 0x0064, // d + 0x65, 0x0065, // e + 0x66, 0x0066, // f + 0x67, 0x0067, // g + 0x68, 0x0068, // h + 0x69, 0x0069, // i + 0x6a, 0x006A, // j + 0x6b, 0x006B, // k + 0x6c, 0x006C, // l + 0x6d, 0x006D, // m + 0x6e, 0x006E, // n + 0x6f, 0x006F, // o + 0x70, 0x0070, // p + 0x71, 0x0071, // q + 0x72, 0x0072, // r + 0x73, 0x0073, // s + 0x74, 0x0074, // t + 0x75, 0x0075, // u + 0x76, 0x0076, // v + 0x77, 0x0077, // w + 0x78, 0x0078, // x + 0x79, 0x0079, // y + 0x7a, 0x007A, // z + 0x7b, 0x007B, // braceleft + 0x7c, 0x007C, // bar + 0x7d, 0x007D, // braceright + 0x7e, 0x007E, // asciitilde + 0x82, 0x201A, // quotesinglbase + 0x84, 0x201E, // quotedblbase + 0x85, 0x2026, // ellipsis + 0x86, 0x2020, // dagger + 0x87, 0x2021, // daggerdbl + 0x89, 0x2030, // perthousand + 0x8a, 0x0160, // Scaron + 0x8b, 0x2039, // guilsinglleft + 0x8c, 0x015A, // Sacute + 0x8d, 0x0164, // Tcaron + 0x8e, 0x017D, // Zcaron + 0x8f, 0x0179, // Zacute + 0x91, 0x2018, // quoteleft + 0x92, 0x2019, // quoteright + 0x93, 0x201C, // quotedblleft + 0x94, 0x201D, // quotedblright + 0x95, 0x2022, // bullet + 0x96, 0x2013, // endash + 0x97, 0x2014, // emdash + 0x99, 0x2122, // trademark + 0x9a, 0x0161, // scaron + 0x9b, 0x203A, // guilsinglright + 0x9c, 0x015B, // sacute + 0x9d, 0x0165, // tcaron + 0x9e, 0x017E, // zcaron + 0x9f, 0x017A, // zacute + 0xa1, 0x02C7, // caron + 0xa2, 0x02D8, // breve + 0xa3, 0x0141, // Lslash + 0xa4, 0x00A4, // currency + 0xa5, 0x0104, // Aogonek + 0xa6, 0x00A6, // brokenbar + 0xa7, 0x00A7, // section + 0xa8, 0x00A8, // dieresis + 0xa9, 0x00A9, // copyright + 0xaa, 0x0218, // Scommaaccent + 0xab, 0x00AB, // guillemotleft + 0xac, 0x00AC, // logicalnot + 0xae, 0x00AE, // registered + 0xaf, 0x017B, // Zdotaccent + 0xb0, 0x00B0, // degree + 0xb1, 0x00B1, // plusminus + 0xb2, 0x02DB, // ogonek + 0xb3, 0x0142, // lslash + 0xb4, 0x00B4, // acute + 0xb5, 0x00B5, // mu + 0xb5, 0x03BC, // mu + 0xb6, 0x00B6, // paragraph + 0xb7, 0x00B7, // periodcentered + 0xb7, 0x2219, // periodcentered + 0xb8, 0x00B8, // cedilla + 0xb9, 0x0105, // aogonek + 0xba, 0x0219, // scommaaccent + 0xbb, 0x00BB, // guillemotright + 0xbc, 0x013D, // Lcaron + 0xbd, 0x02DD, // hungarumlaut + 0xbe, 0x013E, // lcaron + 0xbf, 0x017C, // zdotaccent + 0xc0, 0x0154, // Racute + 0xc1, 0x00C1, // Aacute + 0xc2, 0x00C2, // Acircumflex + 0xc3, 0x0102, // Abreve + 0xc4, 0x00C4, // Adieresis + 0xc5, 0x0139, // Lacute + 0xc6, 0x0106, // Cacute + 0xc7, 0x00C7, // Ccedilla + 0xc8, 0x010C, // Ccaron + 0xc9, 0x00C9, // Eacute + 0xca, 0x0118, // Eogonek + 0xcb, 0x00CB, // Edieresis + 0xcc, 0x011A, // Ecaron + 0xcd, 0x00CD, // Iacute + 0xce, 0x00CE, // Icircumflex + 0xcf, 0x010E, // Dcaron + 0xd0, 0x0110, // Dcroat + 0xd1, 0x0143, // Nacute + 0xd2, 0x0147, // Ncaron + 0xd3, 0x00D3, // Oacute + 0xd4, 0x00D4, // Ocircumflex + 0xd5, 0x0150, // Ohungarumlaut + 0xd6, 0x00D6, // Odieresis + 0xd7, 0x00D7, // multiply + 0xd8, 0x0158, // Rcaron + 0xd9, 0x016E, // Uring + 0xda, 0x00DA, // Uacute + 0xdb, 0x0170, // Uhungarumlaut + 0xdc, 0x00DC, // Udieresis + 0xdd, 0x00DD, // Yacute + 0xde, 0x0162, // Tcommaaccent + 0xde, 0x021A, // Tcommaaccent + 0xdf, 0x00DF, // germandbls + 0xe0, 0x0155, // racute + 0xe1, 0x00E1, // aacute + 0xe2, 0x00E2, // acircumflex + 0xe3, 0x0103, // abreve + 0xe4, 0x00E4, // adieresis + 0xe5, 0x013A, // lacute + 0xe6, 0x0107, // cacute + 0xe7, 0x00E7, // ccedilla + 0xe8, 0x010D, // ccaron + 0xe9, 0x00E9, // eacute + 0xea, 0x0119, // eogonek + 0xeb, 0x00EB, // edieresis + 0xec, 0x011B, // ecaron + 0xed, 0x00ED, // iacute + 0xee, 0x00EE, // icircumflex + 0xef, 0x010F, // dcaron + 0xf0, 0x0111, // dcroat + 0xf1, 0x0144, // nacute + 0xf2, 0x0148, // ncaron + 0xf3, 0x00F3, // oacute + 0xf4, 0x00F4, // ocircumflex + 0xf5, 0x0151, // ohungarumlaut + 0xf6, 0x00F6, // odieresis + 0xf7, 0x00F7, // divide + 0xf8, 0x0159, // rcaron + 0xf9, 0x016F, // uring + 0xfa, 0x00FA, // uacute + 0xfb, 0x0171, // uhungarumlaut + 0xfc, 0x00FC, // udieresis + 0xfd, 0x00FD, // yacute + 0xfe, 0x0163, // tcommaaccent + 0xfe, 0x021B, // tcommaaccent + 0xff, 0x02D9, // dotaccent + }; + + private static final int[] encMacRomanEncoding + = { + 0x20, 0x0020, // space + 0x20, 0x00A0, // space + 0x21, 0x0021, // exclam + 0x22, 0x0022, // quotedbl + 0x23, 0x0023, // numbersign + 0x24, 0x0024, // dollar + 0x25, 0x0025, // percent + 0x26, 0x0026, // ampersand + 0x27, 0x0027, // quotesingle + 0x28, 0x0028, // parenleft + 0x29, 0x0029, // parenright + 0x2a, 0x002A, // asterisk + 0x2b, 0x002B, // plus + 0x2c, 0x002C, // comma + 0x2d, 0x002D, // hyphen + 0x2d, 0x00AD, // hyphen + 0x2e, 0x002E, // period + 0x2f, 0x002F, // slash + 0x30, 0x0030, // zero + 0x31, 0x0031, // one + 0x32, 0x0032, // two + 0x33, 0x0033, // three + 0x34, 0x0034, // four + 0x35, 0x0035, // five + 0x36, 0x0036, // six + 0x37, 0x0037, // seven + 0x38, 0x0038, // eight + 0x39, 0x0039, // nine + 0x3a, 0x003A, // colon + 0x3b, 0x003B, // semicolon + 0x3c, 0x003C, // less + 0x3d, 0x003D, // equal + 0x3e, 0x003E, // greater + 0x3f, 0x003F, // question + 0x40, 0x0040, // at + 0x41, 0x0041, // A + 0x42, 0x0042, // B + 0x43, 0x0043, // C + 0x44, 0x0044, // D + 0x45, 0x0045, // E + 0x46, 0x0046, // F + 0x47, 0x0047, // G + 0x48, 0x0048, // H + 0x49, 0x0049, // I + 0x4a, 0x004A, // J + 0x4b, 0x004B, // K + 0x4c, 0x004C, // L + 0x4d, 0x004D, // M + 0x4e, 0x004E, // N + 0x4f, 0x004F, // O + 0x50, 0x0050, // P + 0x51, 0x0051, // Q + 0x52, 0x0052, // R + 0x53, 0x0053, // S + 0x54, 0x0054, // T + 0x55, 0x0055, // U + 0x56, 0x0056, // V + 0x57, 0x0057, // W + 0x58, 0x0058, // X + 0x59, 0x0059, // Y + 0x5a, 0x005A, // Z + 0x5b, 0x005B, // bracketleft + 0x5c, 0x005C, // backslash + 0x5d, 0x005D, // bracketright + 0x5e, 0x005E, // asciicircum + 0x5f, 0x005F, // underscore + 0x60, 0x0060, // grave + 0x61, 0x0061, // a + 0x62, 0x0062, // b + 0x63, 0x0063, // c + 0x64, 0x0064, // d + 0x65, 0x0065, // e + 0x66, 0x0066, // f + 0x67, 0x0067, // g + 0x68, 0x0068, // h + 0x69, 0x0069, // i + 0x6a, 0x006A, // j + 0x6b, 0x006B, // k + 0x6c, 0x006C, // l + 0x6d, 0x006D, // m + 0x6e, 0x006E, // n + 0x6f, 0x006F, // o + 0x70, 0x0070, // p + 0x71, 0x0071, // q + 0x72, 0x0072, // r + 0x73, 0x0073, // s + 0x74, 0x0074, // t + 0x75, 0x0075, // u + 0x76, 0x0076, // v + 0x77, 0x0077, // w + 0x78, 0x0078, // x + 0x79, 0x0079, // y + 0x7b, 0x007B, // braceleft + 0x7c, 0x007C, // bar + 0x7d, 0x007D, // braceright + 0x7e, 0x007E, // asciitilde + 0x80, 0x00C4, // Adieresis + 0x81, 0x00C5, // Aring + 0x82, 0x00C7, // Ccedilla + 0x83, 0x00C9, // Eacute + 0x84, 0x00D1, // Ntilde + 0x85, 0x00D6, // Odieresis + 0x86, 0x00DC, // Udieresis + 0x87, 0x00E1, // aacute + 0x88, 0x00E0, // agrave + 0x89, 0x00E2, // acircumflex + 0x8a, 0x00E4, // adieresis + 0x8b, 0x00E3, // atilde + 0x8c, 0x00E5, // aring + 0x8d, 0x00E7, // ccedilla + 0x8e, 0x00E9, // eacute + 0x8f, 0x00E8, // egrave + 0x90, 0x00EA, // ecircumflex + 0x91, 0x00EB, // edieresis + 0x92, 0x00ED, // iacute + 0x93, 0x00EC, // igrave + 0x94, 0x00EE, // icircumflex + 0x95, 0x00EF, // idieresis + 0x96, 0x00F1, // ntilde + 0x97, 0x00F3, // oacute + 0x98, 0x00F2, // ograve + 0x99, 0x00F4, // ocircumflex + 0x9a, 0x00F6, // odieresis + 0x9b, 0x00F5, // otilde + 0x9c, 0x00FA, // uacute + 0x9d, 0x00F9, // ugrave + 0x9e, 0x00FB, // ucircumflex + 0x9f, 0x00FC, // udieresis + 0xa0, 0x2020, // dagger + 0xa1, 0x00B0, // degree + 0xa2, 0x00A2, // cent + 0xa3, 0x00A3, // sterling + 0xa4, 0x00A7, // section + 0xa5, 0x2022, // bullet + 0xa6, 0x00B6, // paragraph + 0xa7, 0x00DF, // germandbls + 0xa8, 0x00AE, // registered + 0xa9, 0x00A9, // copyright + 0xaa, 0x2122, // trademark + 0xab, 0x00B4, // acute + 0xac, 0x00A8, // dieresis + 0xae, 0x00C6, // AE + 0xaf, 0x00D8, // Oslash + 0xb1, 0x00B1, // plusminus + 0xb5, 0x00B5, // mu + 0xb5, 0x03BC, // mu + 0xbb, 0x00AA, // ordfeminine + 0xbc, 0x00BA, // ordmasculine + 0xbe, 0x00E6, // ae + 0xbf, 0x00F8, // oslash + 0xc0, 0x00BF, // questiondown + 0xc1, 0x00A1, // exclamdown + 0xc2, 0x00AC, // logicalnot + 0xc4, 0x0192, // florin + 0xc7, 0x00AB, // guillemotleft + 0xc8, 0x00BB, // guillemotright + 0xc9, 0x2026, // ellipsis + 0xcb, 0x00C0, // Agrave + 0xcc, 0x00C3, // Atilde + 0xcd, 0x00D5, // Otilde + 0xce, 0x0152, // OE + 0xcf, 0x0153, // oe + 0xd0, 0x2013, // endash + 0xd1, 0x2014, // emdash + 0xd2, 0x201C, // quotedblleft + 0xd3, 0x201D, // quotedblright + 0xd4, 0x2018, // quoteleft + 0xd5, 0x2019, // quoteright + 0xd6, 0x00F7, // divide + 0xd9, 0x0178, // Ydieresis + 0xda, 0x2044, // fraction + 0xda, 0x2215, // fraction + 0xdb, 0x00A4, // currency + 0xdc, 0x2039, // guilsinglleft + 0xdd, 0x203A, // guilsinglright + 0xde, 0xFB01, // fi + 0xdf, 0xFB02, // fl + 0xe0, 0x2021, // daggerdbl + 0xe1, 0x00B7, // periodcentered + 0xe1, 0x2219, // periodcentered + 0xe2, 0x201A, // quotesinglbase + 0xe3, 0x201E, // quotedblbase + 0xe4, 0x2030, // perthousand + 0xe5, 0x00C2, // Acircumflex + 0xe6, 0x00CA, // Ecircumflex + 0xe7, 0x00C1, // Aacute + 0xe8, 0x00CB, // Edieresis + 0xe9, 0x00C8, // Egrave + 0xea, 0x00CD, // Iacute + 0xeb, 0x00CE, // Icircumflex + 0xec, 0x00CF, // Idieresis + 0xed, 0x00CC, // Igrave + 0xee, 0x00D3, // Oacute + 0xef, 0x00D4, // Ocircumflex + 0xf1, 0x00D2, // Ograve + 0xf2, 0x00DA, // Uacute + 0xf3, 0x00DB, // Ucircumflex + 0xf4, 0x00D9, // Ugrave + 0xf5, 0x0131, // dotlessi + 0xf6, 0x02C6, // circumflex + 0xf7, 0x02DC, // tilde + 0xf8, 0x00AF, // macron + 0xf8, 0x02C9, // macron + 0xf9, 0x02D8, // breve + 0xfa, 0x02D9, // dotaccent + 0xfb, 0x02DA, // ring + 0xfc, 0x00B8, // cedilla + 0xfd, 0x02DD, // hungarumlaut + 0xfe, 0x02DB, // ogonek + 0xff, 0x02C7, // caron + 0xd8, 0x00FF, // ydieresis + 0xb4, 0x00A5, // yen + 0x7a, 0x007A, // z + }; + + private static final int[] encWinAnsiEncoding + = { + 0x20, 0x0020, // space + 0x20, 0x00A0, // space + 0x21, 0x0021, // exclam + 0x22, 0x0022, // quotedbl + 0x23, 0x0023, // numbersign + 0x24, 0x0024, // dollar + 0x25, 0x0025, // percent + 0x26, 0x0026, // ampersand + 0x27, 0x0027, // quotesingle + 0x28, 0x0028, // parenleft + 0x29, 0x0029, // parenright + 0x2a, 0x002A, // asterisk + 0x2b, 0x002B, // plus + 0x2c, 0x002C, // comma + 0x2d, 0x002D, // hyphen + 0x2d, 0x00AD, // hyphen + 0x2e, 0x002E, // period + 0x2f, 0x002F, // slash + 0x30, 0x0030, // zero + 0x31, 0x0031, // one + 0x32, 0x0032, // two + 0x33, 0x0033, // three + 0x34, 0x0034, // four + 0x35, 0x0035, // five + 0x36, 0x0036, // six + 0x37, 0x0037, // seven + 0x38, 0x0038, // eight + 0x39, 0x0039, // nine + 0x3a, 0x003A, // colon + 0x3b, 0x003B, // semicolon + 0x3c, 0x003C, // less + 0x3d, 0x003D, // equal + 0x3e, 0x003E, // greater + 0x3f, 0x003F, // question + 0x40, 0x0040, // at + 0x41, 0x0041, // A + 0x42, 0x0042, // B + 0x43, 0x0043, // C + 0x44, 0x0044, // D + 0x45, 0x0045, // E + 0x46, 0x0046, // F + 0x47, 0x0047, // G + 0x48, 0x0048, // H + 0x49, 0x0049, // I + 0x4a, 0x004A, // J + 0x4b, 0x004B, // K + 0x4c, 0x004C, // L + 0x4d, 0x004D, // M + 0x4e, 0x004E, // N + 0x4f, 0x004F, // O + 0x50, 0x0050, // P + 0x51, 0x0051, // Q + 0x52, 0x0052, // R + 0x53, 0x0053, // S + 0x54, 0x0054, // T + 0x55, 0x0055, // U + 0x56, 0x0056, // V + 0x57, 0x0057, // W + 0x58, 0x0058, // X + 0x59, 0x0059, // Y + 0x5a, 0x005A, // Z + 0x5b, 0x005B, // bracketleft + 0x5c, 0x005C, // backslash + 0x5d, 0x005D, // bracketright + 0x5e, 0x005E, // asciicircum + 0x5f, 0x005F, // underscore + 0x60, 0x0060, // grave + 0x61, 0x0061, // a + 0x62, 0x0062, // b + 0x63, 0x0063, // c + 0x64, 0x0064, // d + 0x65, 0x0065, // e + 0x66, 0x0066, // f + 0x67, 0x0067, // g + 0x68, 0x0068, // h + 0x69, 0x0069, // i + 0x6a, 0x006A, // j + 0x6b, 0x006B, // k + 0x6c, 0x006C, // l + 0x6d, 0x006D, // m + 0x6e, 0x006E, // n + 0x6f, 0x006F, // o + 0x70, 0x0070, // p + 0x71, 0x0071, // q + 0x72, 0x0072, // r + 0x73, 0x0073, // s + 0x74, 0x0074, // t + 0x75, 0x0075, // u + 0x76, 0x0076, // v + 0x77, 0x0077, // w + 0x78, 0x0078, // x + 0x79, 0x0079, // y + 0x7a, 0x007A, // z + 0x7b, 0x007B, // braceleft + 0x7c, 0x007C, // bar + 0x7d, 0x007D, // braceright + 0x7e, 0x007E, // asciitilde + 0x80, 0x20AC, // Euro + 0x82, 0x201A, // quotesinglbase + 0x83, 0x0192, // florin + 0x84, 0x201E, // quotedblbase + 0x85, 0x2026, // ellipsis + 0x86, 0x2020, // dagger + 0x87, 0x2021, // daggerdbl + 0x88, 0x02C6, // circumflex + 0x89, 0x2030, // perthousand + 0x8a, 0x0160, // Scaron + 0x8b, 0x2039, // guilsinglleft + 0x8c, 0x0152, // OE + 0x8e, 0x017D, // Zcaron + 0x91, 0x2018, // quoteleft + 0x92, 0x2019, // quoteright + 0x93, 0x201C, // quotedblleft + 0x94, 0x201D, // quotedblright + 0x95, 0x2022, // bullet + 0x96, 0x2013, // endash + 0x97, 0x2014, // emdash + 0x98, 0x02DC, // tilde + 0x99, 0x2122, // trademark + 0x9a, 0x0161, // scaron + 0x9b, 0x203A, // guilsinglright + 0x9c, 0x0153, // oe + 0x9e, 0x017E, // zcaron + 0x9f, 0x0178, // Ydieresis + 0xa1, 0x00A1, // exclamdown + 0xa2, 0x00A2, // cent + 0xa3, 0x00A3, // sterling + 0xa4, 0x00A4, // currency + 0xa5, 0x00A5, // yen + 0xa6, 0x00A6, // brokenbar + 0xa7, 0x00A7, // section + 0xa8, 0x00A8, // dieresis + 0xa9, 0x00A9, // copyright + 0xaa, 0x00AA, // ordfeminine + 0xab, 0x00AB, // guillemotleft + 0xac, 0x00AC, // logicalnot + 0xae, 0x00AE, // registered + 0xaf, 0x00AF, // macron + 0xaf, 0x02C9, // macron + 0xb0, 0x00B0, // degree + 0xb1, 0x00B1, // plusminus + 0xb2, 0x00B2, // twosuperior + 0xb3, 0x00B3, // threesuperior + 0xb4, 0x00B4, // acute + 0xb5, 0x00B5, // mu + 0xb5, 0x03BC, // mu + 0xb6, 0x00B6, // paragraph + 0xb7, 0x00B7, // periodcentered + 0xb7, 0x2219, // periodcentered + 0xb8, 0x00B8, // cedilla + 0xb9, 0x00B9, // onesuperior + 0xba, 0x00BA, // ordmasculine + 0xbb, 0x00BB, // guillemotright + 0xbc, 0x00BC, // onequarter + 0xbd, 0x00BD, // onehalf + 0xbe, 0x00BE, // threequarters + 0xbf, 0x00BF, // questiondown + 0xc0, 0x00C0, // Agrave + 0xc1, 0x00C1, // Aacute + 0xc2, 0x00C2, // Acircumflex + 0xc3, 0x00C3, // Atilde + 0xc4, 0x00C4, // Adieresis + 0xc5, 0x00C5, // Aring + 0xc6, 0x00C6, // AE + 0xc7, 0x00C7, // Ccedilla + 0xc8, 0x00C8, // Egrave + 0xc9, 0x00C9, // Eacute + 0xca, 0x00CA, // Ecircumflex + 0xcb, 0x00CB, // Edieresis + 0xcc, 0x00CC, // Igrave + 0xcd, 0x00CD, // Iacute + 0xce, 0x00CE, // Icircumflex + 0xcf, 0x00CF, // Idieresis + 0xd0, 0x00D0, // Eth + 0xd1, 0x00D1, // Ntilde + 0xd2, 0x00D2, // Ograve + 0xd3, 0x00D3, // Oacute + 0xd4, 0x00D4, // Ocircumflex + 0xd5, 0x00D5, // Otilde + 0xd6, 0x00D6, // Odieresis + 0xd7, 0x00D7, // multiply + 0xd8, 0x00D8, // Oslash + 0xd9, 0x00D9, // Ugrave + 0xda, 0x00DA, // Uacute + 0xdb, 0x00DB, // Ucircumflex + 0xdc, 0x00DC, // Udieresis + 0xdd, 0x00DD, // Yacute + 0xde, 0x00DE, // Thorn + 0xdf, 0x00DF, // germandbls + 0xe0, 0x00E0, // agrave + 0xe1, 0x00E1, // aacute + 0xe2, 0x00E2, // acircumflex + 0xe3, 0x00E3, // atilde + 0xe4, 0x00E4, // adieresis + 0xe5, 0x00E5, // aring + 0xe6, 0x00E6, // ae + 0xe7, 0x00E7, // ccedilla + 0xe8, 0x00E8, // egrave + 0xe9, 0x00E9, // eacute + 0xea, 0x00EA, // ecircumflex + 0xeb, 0x00EB, // edieresis + 0xec, 0x00EC, // igrave + 0xed, 0x00ED, // iacute + 0xee, 0x00EE, // icircumflex + 0xef, 0x00EF, // idieresis + 0xf0, 0x00F0, // eth + 0xf1, 0x00F1, // ntilde + 0xf2, 0x00F2, // ograve + 0xf3, 0x00F3, // oacute + 0xf4, 0x00F4, // ocircumflex + 0xf5, 0x00F5, // otilde + 0xf6, 0x00F6, // odieresis + 0xf7, 0x00F7, // divide + 0xf8, 0x00F8, // oslash + 0xf9, 0x00F9, // ugrave + 0xfa, 0x00FA, // uacute + 0xfb, 0x00FB, // ucircumflex + 0xfc, 0x00FC, // udieresis + 0xfd, 0x00FD, // yacute + 0xfe, 0x00FE, // thorn + 0xff, 0x00FF, // ydieresis + }; + + private static final int[] encPDFDocEncoding + = { + 0x18, 0x02D8, // breve + 0x19, 0x02C7, // caron + 0x1a, 0x02C6, // circumflex + 0x1b, 0x02D9, // dotaccent + 0x1c, 0x02DD, // hungarumlaut + 0x1d, 0x02DB, // ogonek + 0x1e, 0x02DA, // ring + 0x1f, 0x02DC, // tilde + 0x20, 0x0020, // space + 0x20, 0x00A0, // space + 0x21, 0x0021, // exclam + 0x22, 0x0022, // quotedbl + 0x23, 0x0023, // numbersign + 0x24, 0x0024, // dollar + 0x25, 0x0025, // percent + 0x26, 0x0026, // ampersand + 0x27, 0x0027, // quotesingle + 0x28, 0x0028, // parenleft + 0x29, 0x0029, // parenright + 0x2a, 0x002A, // asterisk + 0x2b, 0x002B, // plus + 0x2c, 0x002C, // comma + 0x2d, 0x002D, // hyphen + 0x2d, 0x00AD, // hyphen + 0x2e, 0x002E, // period + 0x2f, 0x002F, // slash + 0x30, 0x0030, // zero + 0x31, 0x0031, // one + 0x32, 0x0032, // two + 0x33, 0x0033, // three + 0x34, 0x0034, // four + 0x35, 0x0035, // five + 0x36, 0x0036, // six + 0x37, 0x0037, // seven + 0x38, 0x0038, // eight + 0x39, 0x0039, // nine + 0x3a, 0x003A, // colon + 0x3b, 0x003B, // semicolon + 0x3c, 0x003C, // less + 0x3d, 0x003D, // equal + 0x3e, 0x003E, // greater + 0x3f, 0x003F, // question + 0x40, 0x0040, // at + 0x41, 0x0041, // A + 0x42, 0x0042, // B + 0x43, 0x0043, // C + 0x44, 0x0044, // D + 0x45, 0x0045, // E + 0x46, 0x0046, // F + 0x47, 0x0047, // G + 0x48, 0x0048, // H + 0x49, 0x0049, // I + 0x4a, 0x004A, // J + 0x4b, 0x004B, // K + 0x4c, 0x004C, // L + 0x4d, 0x004D, // M + 0x4e, 0x004E, // N + 0x4f, 0x004F, // O + 0x50, 0x0050, // P + 0x51, 0x0051, // Q + 0x52, 0x0052, // R + 0x53, 0x0053, // S + 0x54, 0x0054, // T + 0x55, 0x0055, // U + 0x56, 0x0056, // V + 0x57, 0x0057, // W + 0x58, 0x0058, // X + 0x59, 0x0059, // Y + 0x5a, 0x005A, // Z + 0x5b, 0x005B, // bracketleft + 0x5c, 0x005C, // backslash + 0x5d, 0x005D, // bracketright + 0x5e, 0x005E, // asciicircum + 0x5f, 0x005F, // underscore + 0x60, 0x0060, // grave + 0x61, 0x0061, // a + 0x62, 0x0062, // b + 0x63, 0x0063, // c + 0x64, 0x0064, // d + 0x65, 0x0065, // e + 0x66, 0x0066, // f + 0x67, 0x0067, // g + 0x68, 0x0068, // h + 0x69, 0x0069, // i + 0x6a, 0x006A, // j + 0x6b, 0x006B, // k + 0x6c, 0x006C, // l + 0x6d, 0x006D, // m + 0x6e, 0x006E, // n + 0x6f, 0x006F, // o + 0x70, 0x0070, // p + 0x71, 0x0071, // q + 0x72, 0x0072, // r + 0x73, 0x0073, // s + 0x74, 0x0074, // t + 0x75, 0x0075, // u + 0x76, 0x0076, // v + 0x77, 0x0077, // w + 0x78, 0x0078, // x + 0x79, 0x0079, // y + 0x7a, 0x007A, // z + 0x7b, 0x007B, // braceleft + 0x7c, 0x007C, // bar + 0x7d, 0x007D, // braceright + 0x7e, 0x007E, // asciitilde + 0x80, 0x2022, // bullet + 0x81, 0x2020, // dagger + 0x82, 0x2021, // daggerdbl + 0x83, 0x2026, // ellipsis + 0x84, 0x2014, // emdash + 0x85, 0x2013, // endash + 0x86, 0x0192, // florin + 0x87, 0x2044, // fraction + 0x87, 0x2215, // fraction + 0x88, 0x2039, // guilsinglleft + 0x89, 0x203A, // guilsinglright + 0x8a, 0x2212, // minus + 0x8b, 0x2030, // perthousand + 0x8c, 0x201E, // quotedblbase + 0x8d, 0x201C, // quotedblleft + 0x8e, 0x201D, // quotedblright + 0x8f, 0x2018, // quoteleft + 0x90, 0x2019, // quoteright + 0x91, 0x201A, // quotesinglbase + 0x92, 0x2122, // trademark + 0x93, 0xFB01, // fi + 0x94, 0xFB02, // fl + 0x95, 0x0141, // Lslash + 0x96, 0x0152, // OE + 0x97, 0x0160, // Scaron + 0x98, 0x0178, // Ydieresis + 0x99, 0x017D, // Zcaron + 0x9a, 0x0131, // dotlessi + 0x9b, 0x0142, // lslash + 0x9c, 0x0153, // oe + 0x9d, 0x0161, // scaron + 0x9e, 0x017E, // zcaron + 0xa0, 0x20AC, // Euro + 0xa1, 0x00A1, // exclamdown + 0xa2, 0x00A2, // cent + 0xa3, 0x00A3, // sterling + 0xa4, 0x00A4, // currency + 0xa5, 0x00A5, // yen + 0xa6, 0x00A6, // brokenbar + 0xa7, 0x00A7, // section + 0xa8, 0x00A8, // dieresis + 0xa9, 0x00A9, // copyright + 0xaa, 0x00AA, // ordfeminine + 0xab, 0x00AB, // guillemotleft + 0xac, 0x00AC, // logicalnot + 0xae, 0x00AE, // registered + 0xaf, 0x00AF, // macron + 0xaf, 0x02C9, // macron + 0xb0, 0x00B0, // degree + 0xb1, 0x00B1, // plusminus + 0xb2, 0x00B2, // twosuperior + 0xb3, 0x00B3, // threesuperior + 0xb4, 0x00B4, // acute + 0xb5, 0x00B5, // mu + 0xb5, 0x03BC, // mu + 0xb6, 0x00B6, // paragraph + 0xb7, 0x00B7, // periodcentered + 0xb7, 0x2219, // periodcentered + 0xb8, 0x00B8, // cedilla + 0xb9, 0x00B9, // onesuperior + 0xba, 0x00BA, // ordmasculine + 0xbb, 0x00BB, // guillemotright + 0xbc, 0x00BC, // onequarter + 0xbd, 0x00BD, // onehalf + 0xbe, 0x00BE, // threequarters + 0xbf, 0x00BF, // questiondown + 0xc0, 0x00C0, // Agrave + 0xc1, 0x00C1, // Aacute + 0xc2, 0x00C2, // Acircumflex + 0xc3, 0x00C3, // Atilde + 0xc4, 0x00C4, // Adieresis + 0xc5, 0x00C5, // Aring + 0xc6, 0x00C6, // AE + 0xc7, 0x00C7, // Ccedilla + 0xc8, 0x00C8, // Egrave + 0xc9, 0x00C9, // Eacute + 0xca, 0x00CA, // Ecircumflex + 0xcb, 0x00CB, // Edieresis + 0xcc, 0x00CC, // Igrave + 0xcd, 0x00CD, // Iacute + 0xce, 0x00CE, // Icircumflex + 0xcf, 0x00CF, // Idieresis + 0xd0, 0x00D0, // Eth + 0xd1, 0x00D1, // Ntilde + 0xd2, 0x00D2, // Ograve + 0xd3, 0x00D3, // Oacute + 0xd4, 0x00D4, // Ocircumflex + 0xd5, 0x00D5, // Otilde + 0xd6, 0x00D6, // Odieresis + 0xd7, 0x00D7, // multiply + 0xd8, 0x00D8, // Oslash + 0xd9, 0x00D9, // Ugrave + 0xda, 0x00DA, // Uacute + 0xdb, 0x00DB, // Ucircumflex + 0xdc, 0x00DC, // Udieresis + 0xdd, 0x00DD, // Yacute + 0xde, 0x00DE, // Thorn + 0xdf, 0x00DF, // germandbls + 0xe0, 0x00E0, // agrave + 0xe1, 0x00E1, // aacute + 0xe2, 0x00E2, // acircumflex + 0xe3, 0x00E3, // atilde + 0xe4, 0x00E4, // adieresis + 0xe5, 0x00E5, // aring + 0xe6, 0x00E6, // ae + 0xe7, 0x00E7, // ccedilla + 0xe8, 0x00E8, // egrave + 0xe9, 0x00E9, // eacute + 0xea, 0x00EA, // ecircumflex + 0xeb, 0x00EB, // edieresis + 0xec, 0x00EC, // igrave + 0xed, 0x00ED, // iacute + 0xee, 0x00EE, // icircumflex + 0xef, 0x00EF, // idieresis + 0xf0, 0x00F0, // eth + 0xf1, 0x00F1, // ntilde + 0xf2, 0x00F2, // ograve + 0xf3, 0x00F3, // oacute + 0xf4, 0x00F4, // ocircumflex + 0xf5, 0x00F5, // otilde + 0xf6, 0x00F6, // odieresis + 0xf7, 0x00F7, // divide + 0xf8, 0x00F8, // oslash + 0xf9, 0x00F9, // ugrave + 0xfa, 0x00FA, // uacute + 0xfb, 0x00FB, // ucircumflex + 0xfc, 0x00FC, // udieresis + 0xfd, 0x00FD, // yacute + 0xfe, 0x00FE, // thorn + 0xff, 0x00FF, // ydieresis + }; + + private static final int[] encSymbolEncoding + = { + 0x20, 0x0020, // space + 0x20, 0x00A0, // space + 0x21, 0x0021, // exclam + 0x22, 0x2200, // universal + 0x23, 0x0023, // numbersign + 0x24, 0x2203, // existential + 0x25, 0x0025, // percent + 0x26, 0x0026, // ampersand + 0x27, 0x220B, // suchthat + 0x28, 0x0028, // parenleft + 0x29, 0x0029, // parenright + 0x2a, 0x2217, // asteriskmath + 0x2b, 0x002B, // plus + 0x2c, 0x002C, // comma + 0x2d, 0x2212, // minus + 0x2e, 0x002E, // period + 0x2f, 0x002F, // slash + 0x30, 0x0030, // zero + 0x31, 0x0031, // one + 0x32, 0x0032, // two + 0x33, 0x0033, // three + 0x34, 0x0034, // four + 0x35, 0x0035, // five + 0x36, 0x0036, // six + 0x37, 0x0037, // seven + 0x38, 0x0038, // eight + 0x39, 0x0039, // nine + 0x3a, 0x003A, // colon + 0x3b, 0x003B, // semicolon + 0x3c, 0x003C, // less + 0x3d, 0x003D, // equal + 0x3e, 0x003E, // greater + 0x3f, 0x003F, // question + 0x40, 0x2245, // congruent + 0x41, 0x0391, // Alpha + 0x42, 0x0392, // Beta + 0x43, 0x03A7, // Chi + 0x44, 0x2206, // Delta + 0x44, 0x0394, // Delta + 0x45, 0x0395, // Epsilon + 0x46, 0x03A6, // Phi + 0x47, 0x0393, // Gamma + 0x48, 0x0397, // Eta + 0x49, 0x0399, // Iota + 0x4a, 0x03D1, // theta1 + 0x4b, 0x039A, // Kappa + 0x4c, 0x039B, // Lambda + 0x4d, 0x039C, // Mu + 0x4e, 0x039D, // Nu + 0x4f, 0x039F, // Omicron + 0x50, 0x03A0, // Pi + 0x51, 0x0398, // Theta + 0x52, 0x03A1, // Rho + 0x53, 0x03A3, // Sigma + 0x54, 0x03A4, // Tau + 0x55, 0x03A5, // Upsilon + 0x56, 0x03C2, // sigma1 + 0x57, 0x2126, // Omega + 0x57, 0x03A9, // Omega + 0x58, 0x039E, // Xi + 0x59, 0x03A8, // Psi + 0x5a, 0x0396, // Zeta + 0x5b, 0x005B, // bracketleft + 0x5c, 0x2234, // therefore + 0x5d, 0x005D, // bracketright + 0x5e, 0x22A5, // perpendicular + 0x5f, 0x005F, // underscore + 0x60, 0xF8E5, // radicalex + 0x61, 0x03B1, // alpha + 0x62, 0x03B2, // beta + 0x63, 0x03C7, // chi + 0x64, 0x03B4, // delta + 0x65, 0x03B5, // epsilon + 0x66, 0x03C6, // phi + 0x67, 0x03B3, // gamma + 0x68, 0x03B7, // eta + 0x69, 0x03B9, // iota + 0x6a, 0x03D5, // phi1 + 0x6b, 0x03BA, // kappa + 0x6c, 0x03BB, // lambda + 0x6d, 0x00B5, // mu + 0x6d, 0x03BC, // mu + 0x6e, 0x03BD, // nu + 0x6f, 0x03BF, // omicron + 0x70, 0x03C0, // pi + 0x71, 0x03B8, // theta + 0x72, 0x03C1, // rho + 0x73, 0x03C3, // sigma + 0x74, 0x03C4, // tau + 0x75, 0x03C5, // upsilon + 0x76, 0x03D6, // omega1 + 0x77, 0x03C9, // omega + 0x78, 0x03BE, // xi + 0x79, 0x03C8, // psi + 0x7a, 0x03B6, // zeta + 0x7b, 0x007B, // braceleft + 0x7c, 0x007C, // bar + 0x7d, 0x007D, // braceright + 0x7e, 0x223C, // similar + 0xa0, 0x20AC, // Euro + 0xa1, 0x03D2, // Upsilon1 + 0xa2, 0x2032, // minute + 0xa3, 0x2264, // lessequal + 0xa4, 0x2044, // fraction + 0xa4, 0x2215, // fraction + 0xa5, 0x221E, // infinity + 0xa6, 0x0192, // florin + 0xa7, 0x2663, // club + 0xa8, 0x2666, // diamond + 0xa9, 0x2665, // heart + 0xaa, 0x2660, // spade + 0xab, 0x2194, // arrowboth + 0xac, 0x2190, // arrowleft + 0xad, 0x2191, // arrowup + 0xae, 0x2192, // arrowright + 0xaf, 0x2193, // arrowdown + 0xb0, 0x00B0, // degree + 0xb1, 0x00B1, // plusminus + 0xb2, 0x2033, // second + 0xb3, 0x2265, // greaterequal + 0xb4, 0x00D7, // multiply + 0xb5, 0x221D, // proportional + 0xb6, 0x2202, // partialdiff + 0xb7, 0x2022, // bullet + 0xb8, 0x00F7, // divide + 0xb9, 0x2260, // notequal + 0xba, 0x2261, // equivalence + 0xbb, 0x2248, // approxequal + 0xbc, 0x2026, // ellipsis + 0xbd, 0xF8E6, // arrowvertex + 0xbe, 0xF8E7, // arrowhorizex + 0xbf, 0x21B5, // carriagereturn + 0xc0, 0x2135, // aleph + 0xc1, 0x2111, // Ifraktur + 0xc2, 0x211C, // Rfraktur + 0xc3, 0x2118, // weierstrass + 0xc4, 0x2297, // circlemultiply + 0xc5, 0x2295, // circleplus + 0xc6, 0x2205, // emptyset + 0xc7, 0x2229, // intersection + 0xc8, 0x222A, // union + 0xc9, 0x2283, // propersuperset + 0xca, 0x2287, // reflexsuperset + 0xcb, 0x2284, // notsubset + 0xcc, 0x2282, // propersubset + 0xcd, 0x2286, // reflexsubset + 0xce, 0x2208, // element + 0xcf, 0x2209, // notelement + 0xd0, 0x2220, // angle + 0xd1, 0x2207, // gradient + 0xd2, 0xF6DA, // registerserif + 0xd3, 0xF6D9, // copyrightserif + 0xd4, 0xF6DB, // trademarkserif + 0xd5, 0x220F, // product + 0xd6, 0x221A, // radical + 0xd7, 0x22C5, // dotmath + 0xd8, 0x00AC, // logicalnot + 0xd9, 0x2227, // logicaland + 0xda, 0x2228, // logicalor + 0xdb, 0x21D4, // arrowdblboth + 0xdc, 0x21D0, // arrowdblleft + 0xdd, 0x21D1, // arrowdblup + 0xde, 0x21D2, // arrowdblright + 0xdf, 0x21D3, // arrowdbldown + 0xe0, 0x25CA, // lozenge + 0xe1, 0x2329, // angleleft + 0xe2, 0xF8E8, // registersans + 0xe3, 0xF8E9, // copyrightsans + 0xe4, 0xF8EA, // trademarksans + 0xe5, 0x2211, // summation + 0xe6, 0xF8EB, // parenlefttp + 0xe7, 0xF8EC, // parenleftex + 0xe8, 0xF8ED, // parenleftbt + 0xe9, 0xF8EE, // bracketlefttp + 0xea, 0xF8EF, // bracketleftex + 0xeb, 0xF8F0, // bracketleftbt + 0xec, 0xF8F1, // bracelefttp + 0xed, 0xF8F2, // braceleftmid + 0xee, 0xF8F3, // braceleftbt + 0xef, 0xF8F4, // braceex + 0xf1, 0x232A, // angleright + 0xf2, 0x222B, // integral + 0xf3, 0x2320, // integraltp + 0xf4, 0xF8F5, // integralex + 0xf5, 0x2321, // integralbt + 0xf6, 0xF8F6, // parenrighttp + 0xf7, 0xF8F7, // parenrightex + 0xf8, 0xF8F8, // parenrightbt + 0xf9, 0xF8F9, // bracketrighttp + 0xfa, 0xF8FA, // bracketrightex + 0xfb, 0xF8FB, // bracketrightbt + 0xfc, 0xF8FC, // bracerighttp + 0xfd, 0xF8FD, // bracerightmid + 0xfe, 0xF8FE, // bracerightbt + }; + + private static final int[] encZapfDingbatsEncoding + = { + 0x20, 0x0020, // space + 0x20, 0x00A0, // space + 0x21, 0x2701, // a1 + 0x22, 0x2702, // a2 + 0x23, 0x2703, // a202 + 0x24, 0x2704, // a3 + 0x25, 0x260E, // a4 + 0x26, 0x2706, // a5 + 0x27, 0x2707, // a119 + 0x28, 0x2708, // a118 + 0x29, 0x2709, // a117 + 0x2A, 0x261B, // a11 + 0x2B, 0x261E, // a12 + 0x2C, 0x270C, // a13 + 0x2D, 0x270D, // a14 + 0x2E, 0x270E, // a15 + 0x2F, 0x270F, // a16 + 0x30, 0x2710, // a105 + 0x31, 0x2711, // a17 + 0x32, 0x2712, // a18 + 0x33, 0x2713, // a19 + 0x34, 0x2714, // a20 + 0x35, 0x2715, // a21 + 0x36, 0x2716, // a22 + 0x37, 0x2717, // a23 + 0x38, 0x2718, // a24 + 0x39, 0x2719, // a25 + 0x3A, 0x271A, // a26 + 0x3B, 0x271B, // a27 + 0x3C, 0x271C, // a28 + 0x3D, 0x271D, // a6 + 0x3E, 0x271E, // a7 + 0x3F, 0x271F, // a8 + 0x40, 0x2720, // a9 + 0x41, 0x2721, // a10 + 0x42, 0x2722, // a29 + 0x43, 0x2723, // a30 + 0x44, 0x2724, // a31 + 0x45, 0x2725, // a32 + 0x46, 0x2726, // a33 + 0x47, 0x2727, // a34 + 0x48, 0x2605, // a35 + 0x49, 0x2729, // a36 + 0x4A, 0x272A, // a37 + 0x4B, 0x272B, // a38 + 0x4C, 0x272C, // a39 + 0x4D, 0x272D, // a40 + 0x4E, 0x272E, // a41 + 0x4F, 0x272F, // a42 + 0x50, 0x2730, // a43 + 0x51, 0x2731, // a44 + 0x52, 0x2732, // a45 + 0x53, 0x2733, // a46 + 0x54, 0x2734, // a47 + 0x55, 0x2735, // a48 + 0x56, 0x2736, // a49 + 0x57, 0x2737, // a50 + 0x58, 0x2738, // a51 + 0x59, 0x2739, // a52 + 0x5A, 0x273A, // a53 + 0x5B, 0x273B, // a54 + 0x5C, 0x273C, // a55 + 0x5D, 0x273D, // a56 + 0x5E, 0x273E, // a57 + 0x5F, 0x273F, // a58 + 0x60, 0x2740, // a59 + 0x61, 0x2741, // a60 + 0x62, 0x2742, // a61 + 0x63, 0x2743, // a62 + 0x64, 0x2744, // a63 + 0x65, 0x2745, // a64 + 0x66, 0x2746, // a65 + 0x67, 0x2747, // a66 + 0x68, 0x2748, // a67 + 0x69, 0x2749, // a68 + 0x6A, 0x274A, // a69 + 0x6B, 0x274B, // a70 + 0x6C, 0x25CF, // a71 + 0x6D, 0x274D, // a72 + 0x6E, 0x25A0, // a73 + 0x6F, 0x274F, // a74 + 0x70, 0x2750, // a203 + 0x71, 0x2751, // a75 + 0x72, 0x2752, // a204 + 0x73, 0x25B2, // a76 + 0x74, 0x25BC, // a77 + 0x75, 0x25C6, // a78 + 0x76, 0x2756, // a79 + 0x77, 0x25D7, // a81 + 0x78, 0x2758, // a82 + 0x79, 0x2759, // a83 + 0x7A, 0x275A, // a84 + 0x7B, 0x275B, // a97 + 0x7C, 0x275C, // a98 + 0x7D, 0x275D, // a99 + 0x7E, 0x275E, // a100 + 0x80, 0xF8D7, // a89 + 0x81, 0xF8D8, // a90 + 0x82, 0xF8D9, // a93 + 0x83, 0xF8DA, // a94 + 0x84, 0xF8DB, // a91 + 0x85, 0xF8DC, // a92 + 0x86, 0xF8DD, // a205 + 0x87, 0xF8DE, // a85 + 0x88, 0xF8DF, // a206 + 0x89, 0xF8E0, // a86 + 0x8A, 0xF8E1, // a87 + 0x8B, 0xF8E2, // a88 + 0x8C, 0xF8E3, // a95 + 0x8D, 0xF8E4, // a96 + 0xA1, 0x2761, // a101 + 0xA2, 0x2762, // a102 + 0xA3, 0x2763, // a103 + 0xA4, 0x2764, // a104 + 0xA5, 0x2765, // a106 + 0xA6, 0x2766, // a107 + 0xA7, 0x2767, // a108 + 0xA8, 0x2663, // a112 + 0xA9, 0x2666, // a111 + 0xAA, 0x2665, // a110 + 0xAB, 0x2660, // a109 + 0xAC, 0x2460, // a120 + 0xAD, 0x2461, // a121 + 0xAE, 0x2462, // a122 + 0xAF, 0x2463, // a123 + 0xB0, 0x2464, // a124 + 0xB1, 0x2465, // a125 + 0xB2, 0x2466, // a126 + 0xB3, 0x2467, // a127 + 0xB4, 0x2468, // a128 + 0xB5, 0x2469, // a129 + 0xB6, 0x2776, // a130 + 0xB7, 0x2777, // a131 + 0xB8, 0x2778, // a132 + 0xB9, 0x2779, // a133 + 0xBA, 0x277A, // a134 + 0xBB, 0x277B, // a135 + 0xBC, 0x277C, // a136 + 0xBD, 0x277D, // a137 + 0xBE, 0x277E, // a138 + 0xBF, 0x277F, // a139 + 0xC0, 0x2780, // a140 + 0xC1, 0x2781, // a141 + 0xC2, 0x2782, // a142 + 0xC3, 0x2783, // a143 + 0xC4, 0x2784, // a144 + 0xC5, 0x2785, // a145 + 0xC6, 0x2786, // a146 + 0xC7, 0x2787, // a147 + 0xC8, 0x2788, // a148 + 0xC9, 0x2789, // a149 + 0xCA, 0x278A, // a150 + 0xCB, 0x278B, // a151 + 0xCC, 0x278C, // a152 + 0xCD, 0x278D, // a153 + 0xCE, 0x278E, // a154 + 0xCF, 0x278F, // a155 + 0xD0, 0x2790, // a156 + 0xD1, 0x2791, // a157 + 0xD2, 0x2792, // a158 + 0xD3, 0x2793, // a159 + 0xD4, 0x2794, // a160 + 0xD5, 0x2192, // a161 + 0xD6, 0x2194, // a163 + 0xD7, 0x2195, // a164 + 0xD8, 0x2798, // a196 + 0xD9, 0x2799, // a165 + 0xDA, 0x279A, // a192 + 0xDB, 0x279B, // a166 + 0xDC, 0x279C, // a167 + 0xDD, 0x279D, // a168 + 0xDE, 0x279E, // a169 + 0xDF, 0x279F, // a170 + 0xE0, 0x27A0, // a171 + 0xE1, 0x27A1, // a172 + 0xE2, 0x27A2, // a173 + 0xE3, 0x27A3, // a162 + 0xE4, 0x27A4, // a174 + 0xE5, 0x27A5, // a175 + 0xE6, 0x27A6, // a176 + 0xE7, 0x27A7, // a177 + 0xE8, 0x27A8, // a178 + 0xE9, 0x27A9, // a179 + 0xEA, 0x27AA, // a193 + 0xEB, 0x27AB, // a180 + 0xEC, 0x27AC, // a199 + 0xED, 0x27AD, // a181 + 0xEE, 0x27AE, // a200 + 0xEF, 0x27AF, // a182 + 0xF1, 0x27B1, // a201 + 0xF2, 0x27B2, // a183 + 0xF3, 0x27B3, // a184 + 0xF4, 0x27B4, // a197 + 0xF5, 0x27B5, // a185 + 0xF6, 0x27B6, // a194 + 0xF7, 0x27B7, // a198 + 0xF8, 0x27B8, // a186 + 0xF9, 0x27B9, // a195 + 0xFA, 0x27BA, // a187 + 0xFB, 0x27BB, // a188 + 0xFC, 0x27BC, // a189 + 0xFD, 0x27BD, // a190 + 0xFE, 0x27BE, // a191 + }; + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Font.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Font.java new file mode 100644 index 0000000..3de98dc --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Font.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 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. + * + */ + +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> getKerning() { + if (metric.hasKerningInfo()) { + return metric.getKerningInfo(); + } else { + return Collections.emptyMap(); + } + } + + /** + * Returns the amount of kerning between two characters. + *

+ * 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 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. + *

+ * The value returned measures in pt. So it is already adjusted for font size. + * + * @param ch1 first character + * @param ch2 second character + * @return the distance to adjust for kerning, 0 if there's no kerning + */ + public int getKernValue(int ch1, int ch2) { + // TODO !BMP + if (ch1 > 0x10000) { + return 0; + } else if ((ch1 >= 0xD800) && (ch1 <= 0xE000)) { + return 0; + } else if (ch2 > 0x10000) { + return 0; + } else if ((ch2 >= 0xD800) && (ch2 <= 0xE000)) { + return 0; + } else { + return getKernValue((char) ch1, (char) ch2); + } + } + + /** + * Returns the width of a character + * + * @param charnum character to look up + * @return width of the character + */ + public int getWidth(int charnum) { + // returns width of given character number in millipoints + return (metric.getWidth(charnum, fontSize) / 1000); + } + + /** + * Map a java character (unicode) to a font character. + * Default uses CodePointMapping. + * + * @param c character to map + * @return the mapped character + */ + public char mapChar(char c) { + + if (metric instanceof Typeface) { + return ((Typeface) metric).mapChar(c); + } + + // Use default CodePointMapping + char d = CodePointMapping.getMapping("WinAnsiEncoding").mapChar(c); + if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) { + c = d; + } else { + c = Typeface.NOT_FOUND; + } + + return c; + } + + /** + * Determines whether this font contains a particular character/glyph. + * + * @param c character to check + * @return True if the character is supported, Falso otherwise + */ + public boolean hasChar(char c) { + if (metric instanceof Typeface) { + return ((Typeface) metric).hasChar(c); + } else { + // Use default CodePointMapping + return (CodePointMapping.getMapping("WinAnsiEncoding").mapChar(c) > 0); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringBuffer sbuf = new StringBuffer(super.toString()); + sbuf.append('{'); + sbuf.append(fontName); + sbuf.append(','); + sbuf.append(fontSize); + sbuf.append('}'); + return sbuf.toString(); + } + + /** + * Helper method for getting the width of a unicode char + * from the current fontstate. + * This also performs some guessing on widths on various + * versions of space that might not exists in the font. + * + * @param c character to inspect + * @return the width of the character or -1 if no width available + */ + public int getCharWidth(char c) { + int width; + + if ((c == '\n') || (c == '\r') || (c == '\t') || (c == '\u00A0')) { + width = getCharWidth(' '); + } else { + if (hasChar(c)) { + int mappedChar = mapChar(c); + width = getWidth(mappedChar); + } else { + width = -1; + } + if (width <= 0) { + // Estimate the width of spaces not represented in + // the font + int em = getFontSize(); //http://en.wikipedia.org/wiki/Em_(typography) + int en = em / 2; //http://en.wikipedia.org/wiki/En_(typography) + + if (c == ' ') { + width = em; + } else if (c == '\u2000') { + width = en; + } else if (c == '\u2001') { + width = em; + } else if (c == '\u2002') { + width = em / 2; + } else if (c == '\u2003') { + width = getFontSize(); + } else if (c == '\u2004') { + width = em / 3; + } else if (c == '\u2005') { + width = em / 4; + } else if (c == '\u2006') { + width = em / 6; + } else if (c == '\u2007') { + width = getCharWidth('0'); + } else if (c == '\u2008') { + width = getCharWidth('.'); + } else if (c == '\u2009') { + width = em / 5; + } else if (c == '\u200A') { + width = em / 10; + } else if (c == '\u200B') { + width = 0; + } else if (c == '\u202F') { + width = getCharWidth(' ') / 2; + } else if (c == '\u2060') { + width = 0; + } else if (c == '\u3000') { + width = getCharWidth(' ') * 2; + } else if (c == '\ufeff') { + width = 0; + } else { + //Will be internally replaced by "#" if not found + width = getWidth(mapChar(c)); + } + } + } + + return width; + } + + /** + * Helper method for getting the width of a unicode char + * from the current fontstate. + * This also performs some guessing on widths on various + * versions of space that might not exists in the font. + * + * @param c character to inspect + * @return the width of the character or -1 if no width available + */ + public int getCharWidth(int c) { + if (c < 0x10000) { + return getCharWidth((char) c); + } else { + // TODO !BMP + return -1; + } + } + + /** + * Calculates the word width. + * + * @param word text to get width for + * @return the width of the text + */ + public int getWordWidth(String word) { + if (word == null) { + return 0; + } + int wordLength = word.length(); + int width = 0; + char[] characters = new char[wordLength]; + word.getChars(0, wordLength, characters, 0); + for (int i = 0; i < wordLength; i++) { + width += getCharWidth(characters[i]); + } + return width; + } + + /** + * {@inheritDoc} + */ + public boolean performsSubstitution() { + if (metric instanceof Substitutable) { + Substitutable s = (Substitutable) metric; + return s.performsSubstitution(); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + public CharSequence performSubstitution(CharSequence cs, + String script, String language, List associations, boolean retainControls) { + if (metric instanceof Substitutable) { + Substitutable s = (Substitutable) metric; + return s.performSubstitution(cs, script, language, associations, retainControls); + } else { + throw new UnsupportedOperationException(); + } + } + + /** + * {@inheritDoc} + */ + public CharSequence reorderCombiningMarks(CharSequence cs, int[][] gpa, + String script, String language, List associations) { + if (metric instanceof Substitutable) { + Substitutable s = (Substitutable) metric; + return s.reorderCombiningMarks(cs, gpa, script, language, associations); + } else { + throw new UnsupportedOperationException(); + } + } + + /** + * {@inheritDoc} + */ + public boolean performsPositioning() { + if (metric instanceof Positionable) { + Positionable p = (Positionable) metric; + return p.performsPositioning(); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + public int[][] performPositioning(CharSequence cs, String script, String language, int fontSize) { + if (metric instanceof Positionable) { + Positionable p = (Positionable) metric; + return p.performPositioning(cs, script, language, fontSize); + } else { + throw new UnsupportedOperationException(); + } + } + + /** + * {@inheritDoc} + */ + public int[][] performPositioning(CharSequence cs, String script, String language) { + return performPositioning(cs, script, language, fontSize); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontMetrics.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontMetrics.java new file mode 100644 index 0000000..4aeaf69 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontMetrics.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 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. + * + */ + +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 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. + *

+ * 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> getKerningInfo(); + + /** + * Returns the distance from the baseline to the center of the underline (negative + * value indicates below baseline). + * + * @param size font size + * @return the position in 1/1000ths of the font size + */ + int getUnderlinePosition(int size); + + /** + * Returns the thickness of the underline. + * + * @param size font size + * @return the thickness in 1/1000ths of the font size + */ + int getUnderlineThickness(int size); + + /** + * Returns the distance from the baseline to the center of the strikeout line + * (negative value indicates below baseline). + * + * @param size font size + * @return the position in 1/1000ths of the font size + */ + int getStrikeoutPosition(int size); + + /** + * Returns the thickness of the strikeout line. + * + * @param size font size + * @return the thickness in 1/1000ths of the font size + */ + int getStrikeoutThickness(int size); + + /** + * Determine if metrics supports specific feature in specified font table. + * + * @param tableType type of table (GSUB, GPOS, ...), see GlyphTable.GLYPH_TABLE_TYPE_* + * @param script to qualify feature lookup + * @param language to qualify feature lookup + * @param feature to test + * @return true if feature supported (and has at least one lookup) + */ + boolean hasFeature(int tableType, String script, String language, String feature); + + /** + * Determines whether the font is a multibyte font. + * + * @return True if it is multibyte + */ + boolean isMultiByte(); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontTriplet.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontTriplet.java new file mode 100644 index 0000000..0942143 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontTriplet.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 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. + * + */ + +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, Serializable { + + public static final FontTriplet DEFAULT_FONT_TRIPLET + = new FontTriplet("any", Font.STYLE_NORMAL, Font.WEIGHT_NORMAL); + + /** + * serial version UID + */ + private static final long serialVersionUID = 1168991106658033508L; + + private String name; + private String style; + private int weight; + private int priority; // priority of this triplet/font mapping + + //This is only a cache + private transient String key; + + public FontTriplet() { + this(null, null, 0); + } + + /** + * Creates a new font triplet. + * + * @param name font name + * @param style font style (normal, italic etc.) + * @param weight font weight (100, 200, 300...800, 900) + */ + public FontTriplet(String name, String style, int weight) { + this(name, style, weight, Font.PRIORITY_DEFAULT); + } + + /** + * Creates a new font triplet. + * + * @param name font name + * @param style font style (normal, italic etc.) + * @param weight font weight (100, 200, 300...800, 900) + * @param priority priority of this triplet/font mapping + */ + public FontTriplet(String name, String style, int weight, int priority) { + this.name = name; + this.style = style; + this.weight = weight; + this.priority = priority; + } + + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + } + + /** + * @return the font name + */ + public String getName() { + return name; + } + + /** + * @return the font style + */ + public String getStyle() { + return style; + } + + /** + * @return the font weight + */ + public int getWeight() { + return weight; + } + + /** + * @return the priority of this triplet/font mapping + */ + public int getPriority() { + return priority; + } + + private String getKey() { + if (this.key == null) { + //This caches the combined key + this.key = getName() + "," + getStyle() + "," + getWeight(); + } + return this.key; + } + + /** + * {@inheritDoc} + */ + public int compareTo(FontTriplet o) { + return getKey().compareTo(o.getKey()); + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + return toString().hashCode(); + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj == null) { + return false; + } else if (obj == this) { + return true; + } else { + if (obj instanceof FontTriplet) { + FontTriplet other = (FontTriplet) obj; + return (getName().equals(other.getName()) + && getStyle().equals(other.getStyle()) + && (getWeight() == other.getWeight())); + } + } + return false; + } + + /** + * {@inheritDoc} + */ + public String toString() { + return getKey(); + } + + /** + * Matcher interface for {@link FontTriplet}. + */ + public interface Matcher { + + /** + * Indicates whether the given {@link FontTriplet} matches a particular criterium. + * + * @param triplet the font triplet + * @return true if the font triplet is a match + */ + boolean matches(FontTriplet triplet); + } +} + diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontType.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontType.java new file mode 100644 index 0000000..7944377 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontType.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.fonts; + +/** + * This class enumerates all supported font types. + */ +public class FontType { + + /** + * Collective identifier for "other" font types + */ + public static final FontType OTHER = new FontType("Other", 0); + /** + * Adobe Type 0 fonts (composite font) + */ + public static final FontType TYPE0 = new FontType("Type0", 1); + /** + * Adobe Type 1 fonts + */ + public static final FontType TYPE1 = new FontType("Type1", 2); + /** + * Adobe Multiple Master Type 1 fonts + */ + public static final FontType MMTYPE1 = new FontType("MMType1", 3); + /** + * Adobe Type 3 fonts ("user-defined" fonts) + */ + public static final FontType TYPE3 = new FontType("Type3", 4); + /** + * TrueType fonts + */ + public static final FontType TRUETYPE = new FontType("TrueType", 5); + + public static final FontType TYPE1C = new FontType("Type1C", 6); + + public static final FontType CIDTYPE0 = new FontType("CIDFontType0", 7); + + private final String name; + private final int value; + + /** + * Construct a font type. + * + * @param name a font type name + * @param value a font type value + */ + protected FontType(String name, int value) { + this.name = name; + this.value = value; + } + + /** + * Returns the FontType by name. + * + * @param name Name of the font type to look up + * @return the font type + */ + public static FontType byName(String name) { + if (name.equalsIgnoreCase(FontType.OTHER.getName())) { + return FontType.OTHER; + } else if (name.equalsIgnoreCase(FontType.TYPE0.getName())) { + return FontType.TYPE0; + } else if (name.equalsIgnoreCase(FontType.TYPE1.getName())) { + return FontType.TYPE1; + } else if (name.equalsIgnoreCase(FontType.MMTYPE1.getName())) { + return FontType.MMTYPE1; + } else if (name.equalsIgnoreCase(FontType.TYPE3.getName())) { + return FontType.TYPE3; + } else if (name.equalsIgnoreCase(FontType.TRUETYPE.getName())) { + return FontType.TRUETYPE; + } else { + throw new IllegalArgumentException("Invalid font type: " + name); + } + } + + /** + * Returns the FontType by value. + * + * @param value Value of the font type to look up + * @return the font type + */ + public static FontType byValue(int value) { + if (value == FontType.OTHER.getValue()) { + return FontType.OTHER; + } else if (value == FontType.TYPE0.getValue()) { + return FontType.TYPE0; + } else if (value == FontType.TYPE1.getValue()) { + return FontType.TYPE1; + } else if (value == FontType.MMTYPE1.getValue()) { + return FontType.MMTYPE1; + } else if (value == FontType.TYPE3.getValue()) { + return FontType.TYPE3; + } else if (value == FontType.TRUETYPE.getValue()) { + return FontType.TRUETYPE; + } else { + throw new IllegalArgumentException("Invalid font type: " + value); + } + } + + /** + * Returns the name + * + * @return the name + */ + public String getName() { + return name; + } + + /** + * Returns the value + * + * @return the value + */ + public int getValue() { + return value; + } + + @Override + public String toString() { + return name; + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontUtil.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontUtil.java new file mode 100644 index 0000000..8970ade --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/FontUtil.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.fonts; + +/** + * Font utilities. + */ +public final class FontUtil { + + private FontUtil() { + } + + /** + * Parses an CSS2 (SVG and XSL-FO) font weight (normal, bold, 100-900) to + * an integer. + * See http://www.w3.org/TR/REC-CSS2/fonts.html#propdef-font-weight + * TODO: Implement "lighter" and "bolder". + * + * @param text the font weight to parse + * @return an integer between 100 and 900 (100, 200, 300...) + */ + public static int parseCSS2FontWeight(String text) { + int weight = 400; + try { + weight = Integer.parseInt(text); + weight = (weight / 100) * 100; + weight = Math.max(weight, 100); + weight = Math.min(weight, 900); + } catch (NumberFormatException nfe) { + //weight is no number, so convert symbolic name to number + if (text.equals("normal")) { + weight = 400; + } else if (text.equals("bold")) { + weight = 700; + } else { + throw new IllegalArgumentException( + "Illegal value for font weight: '" + + text + + "'. Use one of: 100, 200, 300, " + + "400, 500, 600, 700, 800, 900, " + + "normal (=400), bold (=700)"); + } + } + return weight; + } + + /** + * Removes all white space from a string (used primarily for font names) + * + * @param str the string + * @return the processed result + */ + public static String stripWhiteSpace(String str) { + if (str != null) { + StringBuffer stringBuffer = new StringBuffer(str.length()); + for (int i = 0, strLen = str.length(); i < strLen; i++) { + final char ch = str.charAt(i); + if (ch != ' ' && ch != '\r' && ch != '\n' && ch != '\t') { + stringBuffer.append(ch); + } + } + return stringBuffer.toString(); + } + return str; + } + + /** + * font constituent names which identify a font as being of "italic" style + */ + private static final String[] ITALIC_WORDS = { + Font.STYLE_ITALIC, Font.STYLE_OBLIQUE, Font.STYLE_INCLINED + }; + + /** + * font constituent names which identify a font as being of "light" weight + */ + private static final String[] LIGHT_WORDS = {"light"}; + /** + * font constituent names which identify a font as being of "medium" weight + */ + private static final String[] MEDIUM_WORDS = {"medium"}; + /** + * font constituent names which identify a font as being of "demi/semi" weight + */ + private static final String[] DEMI_WORDS = {"demi", "semi"}; + /** + * font constituent names which identify a font as being of "bold" weight + */ + private static final String[] BOLD_WORDS = {"bold"}; + /** + * font constituent names which identify a font as being of "extra bold" weight + */ + private static final String[] EXTRA_BOLD_WORDS = {"extrabold", "extra bold", "black", + "heavy", "ultra", "super"}; + + /** + * Guesses the font style of a font using its name. + * + * @param fontName the font name + * @return "normal" or "italic" + */ + public static String guessStyle(String fontName) { + if (fontName != null) { + for (int i = 0; i < ITALIC_WORDS.length; i++) { + if (fontName.indexOf(ITALIC_WORDS[i]) != -1) { + return Font.STYLE_ITALIC; + } + } + } + return Font.STYLE_NORMAL; + } + + /** + * Guesses the font weight of a font using its name. + * + * @param fontName the font name + * @return an integer between 100 and 900 + */ + public static int guessWeight(String fontName) { + // weight + int weight = Font.WEIGHT_NORMAL; + + for (int i = 0; i < BOLD_WORDS.length; i++) { + if (fontName.indexOf(BOLD_WORDS[i]) != -1) { + weight = Font.WEIGHT_BOLD; + break; + } + } + for (int i = 0; i < MEDIUM_WORDS.length; i++) { + if (fontName.indexOf(MEDIUM_WORDS[i]) != -1) { + weight = Font.WEIGHT_NORMAL + 100; //500 + break; + } + } + //Search for "semi/demi" before "light", but after "bold" + //(normally semi/demi-bold is meant, but it can also be semi/demi-light) + for (int i = 0; i < DEMI_WORDS.length; i++) { + if (fontName.indexOf(DEMI_WORDS[i]) != -1) { + weight = Font.WEIGHT_BOLD - 100; //600 + break; + } + } + for (int i = 0; i < EXTRA_BOLD_WORDS.length; i++) { + if (fontName.indexOf(EXTRA_BOLD_WORDS[i]) != -1) { + weight = Font.WEIGHT_EXTRA_BOLD; + break; + } + } + for (int i = 0; i < LIGHT_WORDS.length; i++) { + if (fontName.indexOf(LIGHT_WORDS[i]) != -1) { + weight = Font.WEIGHT_LIGHT; + break; + } + } + return weight; + } +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitution.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitution.java new file mode 100644 index 0000000..3b8ea0c --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitution.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.fonts; + +// CSOFF: LineLengthCheck + +/** + *

The GlyphSubstitution 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.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface GlyphSubstitution { + + /** + * Perform glyph substitution at the current index, mutating the substitution state object as required. + * Only the context associated with the current index is processed. + * + * @param ss glyph substitution state object + * @return true if the glyph subtable was applied, meaning that the current context matches the + * associated input context glyph coverage table + */ + boolean substitute(GlyphSubstitutionState ss); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionState.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionState.java new file mode 100644 index 0000000..aa92b59 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionState.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 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. + * + */ + +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; + +/** + *

The GlyphSubstitutionState implements an state object used during glyph substitution + * processing.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 nig input glyphs + * starting at the current position. If lookups are non-null and non-empty, then + * all input glyphs specified by nig are consumed irregardless of + * whether any specified lookup applied. + * + * @param lookups array of matched lookups (or null) + * @param nig number of glyphs in input sequence, starting at current position, to which + * the lookups are to apply, and to be consumed once the application has finished + * @return true if lookups are non-null and non-empty; otherwise, false + */ + public boolean apply(GlyphTable.RuleLookup[] lookups, int nig) { + // int nbg = index; + int nlg = indexLast - (index + nig); + int nog = 0; + if ((lookups != null) && (lookups.length > 0)) { + // apply each rule lookup to extracted input glyph array + for (int i = 0, n = lookups.length; i < n; i++) { + GlyphTable.RuleLookup l = lookups[i]; + if (l != null) { + GlyphTable.LookupTable lt = l.getLookup(); + if (lt != null) { + // perform substitution on a copy of previous state + GlyphSubstitutionState ss = new GlyphSubstitutionState(this); + // apply lookup table substitutions + GlyphSequence gs = lt.substitute(ss, l.getSequenceIndex()); + // replace current input sequence starting at current position with result + if (replaceInput(0, -1, gs)) { + nog = gs.getGlyphCount() - nlg; + } + } + } + } + // output glyphs and associations + putGlyphs(getGlyphs(0, nog, false, null, null, null), getAssociations(0, nog, false, null, null, null), null); + // consume replaced input glyphs + consume(nog); + return true; + } else { + return false; + } + } + + /** + * Apply default application semantices; namely, consume one input glyph, + * writing that glyph (and its association) to the output glyphs (and associations). + */ + public void applyDefault() { + super.applyDefault(); + int gi = getGlyph(); + if (gi != 65535) { + putGlyph(gi, getAssociation(), null); + } + } + + private static IntBuffer growBuffer(IntBuffer ib) { + int capacity = ib.capacity(); + int capacityNew = capacity * 2; + IntBuffer ibNew = IntBuffer.allocate(capacityNew); + ib.rewind(); + return ibNew.put(ib); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionSubtable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionSubtable.java new file mode 100644 index 0000000..2d07b83 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionSubtable.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 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. + * + */ + +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; + +/** + *

The GlyphSubstitutionSubtable implements an abstract base of a glyph substitution subtable, + * providing a default implementation of the GlyphSubstitution interface.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public abstract class GlyphSubstitutionSubtable extends GlyphSubtable implements GlyphSubstitution { + + private static final GlyphSubstitutionState STATE = new GlyphSubstitutionState(); + + /** + * Instantiate a GlyphSubstitutionSubtable. + * + * @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 sequenceIndex is non-negative, then apply subtables only when current position + * matches sequenceIndex in relation to the starting position. Furthermore, upon + * successful application at sequenceIndex, then apply default action for all remaining + * glyphs in input sequence. + * + * @param ss substitution state + * @param sta array of subtables to apply + * @param sequenceIndex if non negative, then apply subtables only at specified sequence index + * @return output glyph sequence + */ + public static final GlyphSequence substitute(GlyphSubstitutionState ss, GlyphSubstitutionSubtable[] sta, + int sequenceIndex) { + int sequenceStart = ss.getPosition(); + boolean appliedOneShot = false; + while (ss.hasNext()) { + boolean applied = false; + if (!appliedOneShot && ss.maybeApplicable()) { + for (int i = 0, n = sta.length; !applied && (i < n); i++) { + if (sequenceIndex < 0) { + applied = ss.apply(sta[i]); + } else if (ss.getPosition() == (sequenceStart + sequenceIndex)) { + applied = ss.apply(sta[i]); + if (applied) { + appliedOneShot = true; + } + } + } + } + if (!applied || !ss.didConsume()) { + ss.applyDefault(); + } + ss.next(); + } + return ss.getOutput(); + } + + /** + * Apply substitutions. + * + * @param gs input glyph sequence + * @param script tag + * @param language tag + * @param feature tag + * @param sta subtable array + * @param sct script context tester + * @return output glyph sequence + */ + public static final GlyphSequence substitute(GlyphSequence gs, String script, String language, String feature, + GlyphSubstitutionSubtable[] sta, ScriptContextTester sct) { + synchronized (STATE) { + return substitute(STATE.reset(gs, script, language, feature, sct), sta, -1); + } + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionTable.java new file mode 100644 index 0000000..1df75af --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubstitutionTable.java @@ -0,0 +1,1790 @@ +/* + * Copyright (C) 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. + * + */ + +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; + +/** + *

The GlyphSubstitutionTable class is a glyph table that implements + * GlyphSubstitution functionality.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 GlyphSubstitutionTable 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/*>*/ 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/**/ 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/**/ 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/**/ 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/**/ 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/**/ 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/**/ 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 Ligature class implements a ligature lookup result in terms of + * a ligature glyph (code) and the N+1... components that comprise the ligature, + * where the Nth 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 N+1... 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 N+1... 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 LigatureSet class implements a set of ligatures. + */ + public static class LigatureSet { + + private final Ligature[] ligatures; + // set of ligatures all of which share the first (implied) component + private final int maxComponents; // maximum number of components (including first) + + /** + * Instantiate a set of ligatures. + * + * @param ligatures collection of ligatures + */ + public LigatureSet(List ligatures) { + this((Ligature[]) ligatures.toArray(new Ligature[ligatures.size()])); + } + + /** + * Instantiate a set of ligatures. + * + * @param ligatures array of ligatures + */ + public LigatureSet(Ligature[] ligatures) { + if (ligatures == null) { + throw new AdvancedTypographicTableFormatException("invalid ligatures, must be non-null array"); + } else { + this.ligatures = ligatures; + int ncMax = -1; + for (int i = 0, n = ligatures.length; i < n; i++) { + Ligature l = ligatures[i]; + int nc = l.getNumComponents() + 1; + if (nc > ncMax) { + ncMax = nc; + } + } + maxComponents = ncMax; + } + } + + /** + * @return array of ligatures in this ligature set + */ + public Ligature[] getLigatures() { + return ligatures; + } + + /** + * @return count of ligatures in this ligature set + */ + public int getNumLigatures() { + return ligatures.length; + } + + /** + * @return maximum number of components in one ligature (including first component) + */ + public int getMaxComponents() { + return maxComponents; + } + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("{ligs={"); + for (int i = 0, n = ligatures.length; i < n; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(ligatures[i]); + } + sb.append("}}"); + return sb.toString(); + } + + } + +} + diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubtable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubtable.java new file mode 100644 index 0000000..65a66fa --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/GlyphSubtable.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 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. + * + */ + +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 + +/** + *

The GlyphSubtable implements an abstract glyph subtable that + * encapsulates identification, type, format, and coverage information.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 null, 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/**/ lookupTables) { + } + + /** + * Map glyph id to coverage index. + * + * @param gid glyph id + * @return the corresponding coverage index of the specified glyph id + */ + public int getCoverageIndex(int gid) { + if (mapping instanceof GlyphCoverageMapping) { + return ((GlyphCoverageMapping) mapping).getCoverageIndex(gid); + } else { + return -1; + } + } + + /** + * Map glyph id to coverage index. + * + * @return the corresponding coverage index of the specified glyph id + */ + public int getCoverageSize() { + if (mapping instanceof GlyphCoverageMapping) { + return ((GlyphCoverageMapping) mapping).getCoverageSize(); + } else { + return 0; + } + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + int hc = sequence; + hc = (hc * 3) + (lookupId.hashCode() ^ hc); + return hc; + } + + /** + * {@inheritDoc} + * + * @return true if the lookup identifier and the sequence number of the specified subtable is the same + * as the lookup identifier and sequence number of this subtable + */ + public boolean equals(Object o) { + if (o instanceof GlyphSubtable) { + GlyphSubtable st = (GlyphSubtable) o; + return lookupId.equals(st.lookupId) && (sequence == st.sequence); + } else { + return false; + } + } + + /** + * {@inheritDoc} + * + * @return the result of comparing the lookup identifier and the sequence number of the specified subtable with + * the lookup identifier and sequence number of this subtable + */ + public int compareTo(Object o) { + int d; + if (o instanceof GlyphSubtable) { + GlyphSubtable st = (GlyphSubtable) o; + if ((d = lookupId.compareTo(st.lookupId)) == 0) { + if (sequence < st.sequence) { + d = -1; + } else if (sequence > st.sequence) { + d = 1; + } + } + } else { + d = -1; + } + return d; + } + + /** + * Determine if any of the specified subtables uses reverse scanning. + * + * @param subtables array of glyph subtables + * @return true if any of the specified subtables uses reverse scanning. + */ + public static boolean usesReverseScan(GlyphSubtable[] subtables) { + if ((subtables == null) || (subtables.length == 0)) { + return false; + } else { + for (int i = 0, n = subtables.length; i < n; i++) { + if (subtables[i].usesReverseScan()) { + return true; + } + } + return false; + } + } + + /** + * Determine consistent flags for a set of subtables. + * + * @param subtables array of glyph subtables + * @return consistent flags + * @throws IllegalStateException if inconsistent flags + */ + public static int getFlags(GlyphSubtable[] subtables) throws IllegalStateException { + if ((subtables == null) || (subtables.length == 0)) { + return 0; + } else { + int flags = 0; + // obtain first non-zero value of flags in array of subtables + for (int i = 0, n = subtables.length; i < n; i++) { + int f = subtables[i].getFlags(); + if (flags == 0) { + flags = f; + break; + } + } + // enforce flag consistency + for (int i = 0, n = subtables.length; i < n; i++) { + int f = subtables[i].getFlags(); + if (f != flags) { + throw new IllegalStateException("inconsistent lookup flags " + f + ", expected " + flags); + } + } + return flags | (usesReverseScan(subtables) ? LF_INTERNAL_USE_REVERSE_SCAN : 0); + } + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Glyphs.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Glyphs.java new file mode 100644 index 0000000..5611101 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Glyphs.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.fonts; + +public class Glyphs { + + /** + * The characters in WinAnsiEncoding + */ + public static final char[] WINANSI_ENCODING = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ' ', '\u0021', + '\"', '\u0023', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '\u002d', '\u002e', '/', '0', '1', '2', '3', '4', + '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', + 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '\u005b', '\\', '\u005d', '^', '_', + '\u2018', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', + 'v', 'w', 'x', 'y', 'z', '\u007b', '\u007c', '\u007d', '\u007e', '\u2022', '\u20ac', '\u2022', '\u201a', '\u0192', + '\u201e', '\u2026', '\u2020', '\u2021', '\u02c6', '\u2030', '\u0160', '\u2039', '\u0152', '\u2022', '\u017d', + '\u2022', '\u2022', '\u2018', '\u2019', '\u201c', '\u201d', '\u2022', '\u2013', '\u2014', '~', '\u2122', '\u0161', + '\u203a', '\u0153', '\u2022', '\u017e', '\u0178', ' ', '\u00a1', '\u00a2', '\u00a3', '\u00a4', '\u00a5', '\u00a6', + '\u00a7', '\u00a8', '\u00a9', '\u00aa', '\u00ab', '\u00ac', '\u00ad', '\u00ae', '\u00af', '\u00b0', '\u00b1', + '\u00b2', '\u00b3', '\u00b4', '\u00b5', '\u00b6', '\u00b7', '\u00b8', '\u00b9', '\u00ba', '\u00bb', '\u00bc', + '\u00bd', '\u00be', '\u00bf', '\u00c0', '\u00c1', '\u00c2', '\u00c3', '\u00c4', '\u00c5', '\u00c6', '\u00c7', + '\u00c8', '\u00c9', '\u00ca', '\u00cb', '\u00cc', '\u00cd', '\u00ce', '\u00cf', '\u00d0', '\u00d1', '\u00d2', + '\u00d3', '\u00d4', '\u00d5', '\u00d6', '\u00d7', '\u00d8', '\u00d9', '\u00da', '\u00db', '\u00dc', '\u00dd', + '\u00de', '\u00df', '\u00e0', '\u00e1', '\u00e2', '\u00e3', '\u00e4', '\u00e5', '\u00e6', '\u00e7', '\u00e8', + '\u00e9', '\u00ea', '\u00eb', '\u00ec', '\u00ed', '\u00ee', '\u00ef', '\u00f0', '\u00f1', '\u00f2', '\u00f3', + '\u00f4', '\u00f5', '\u00f6', '\u00f7', '\u00f8', '\u00f9', '\u00fa', '\u00fb', '\u00fc', '\u00fd', '\u00fe', + '\u00ff' + }; + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFAdvancedTypographicTableReader.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFAdvancedTypographicTableReader.java new file mode 100644 index 0000000..4e788f2 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFAdvancedTypographicTableReader.java @@ -0,0 +1,3393 @@ +/* + * Copyright (C) 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. + * + */ + +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; + +/** + *

OpenType Font (OTF) advanced typographic table reader. Used by @{Link org.apache.fop.fonts.truetype.TTFFile} + * to read advanced typographic tables (GDEF, GSUB, GPOS).

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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/**/ seScripts; + // script-tag => Object[3] : { default-language-tag, List(language-tag), seLanguages } + private transient Map/**/ seLanguages; + // language-tag => Object[2] : { "f", List("f") + private transient Map/*>*/ seFeatures; + // "f" => Object[2] : { feature-tag, List("lu") } + private transient GlyphMappingTable seMapping; // subtable entry mappings + private transient List seEntries; // subtable entry entries + private transient List seSubtables; // subtable entry subtables + + /** + * Construct an OTFAdvancedTypographicTableReader 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/**/ 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/**/ 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/**/ 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/**/ ll, + Map/**/ languages) { + for (Iterator lit = ll.iterator(); lit.hasNext(); ) { + String lt = (String) lit.next(); + constructLookupsLanguage(lookups, st, lt, languages); + } + } + + private Map constructLookups() { + Map/*>*/ 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/**/ 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/**/ 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/**/ 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/**/ subtables = new java.util.ArrayList(); + if (seSubtables != null) { + for (Iterator it = seSubtables.iterator(); it.hasNext(); ) { + Object[] stp = (Object[]) it.next(); + GlyphSubtable st; + if ((st = constructGPOSSubtable(stp)) != null) { + subtables.add(st); + } + } + } + return subtables; + } + + private GlyphSubtable constructGPOSSubtable(Object[] stp) { + GlyphSubtable st = null; + assert (stp != null) && (stp.length == 8); + Integer tt = (Integer) stp[0]; // table type + Integer lt = (Integer) stp[1]; // lookup type + Integer ln = (Integer) stp[2]; // lookup sequence number + Integer lf = (Integer) stp[3]; // lookup flags + Integer sn = (Integer) stp[4]; // subtable sequence number + Integer sf = (Integer) stp[5]; // subtable format + GlyphCoverageTable coverage = (GlyphCoverageTable) stp[6]; + List entries = (List) stp[7]; + if (tt.intValue() == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING) { + int type = GSUBLookupType.getSubtableType(lt.intValue()); + String lid = "lu" + ln.intValue(); + int sequence = sn.intValue(); + int flags = lf.intValue(); + int format = sf.intValue(); + st = GlyphPositioningTable.createSubtable(type, lid, sequence, flags, format, coverage, entries); + } + return st; + } + + private void initATState() { + seScripts = new java.util.LinkedHashMap(); + seLanguages = new java.util.LinkedHashMap(); + seFeatures = new java.util.LinkedHashMap(); + seSubtables = new java.util.ArrayList(); + resetATSubState(); + } + + private void resetATState() { + seScripts = null; + seLanguages = null; + seFeatures = null; + seSubtables = null; + resetATSubState(); + } + + private void initATSubState() { + seMapping = null; + seEntries = new java.util.ArrayList(); + } + + private void extractSESubState(int tableType, int lookupType, int lookupFlags, int lookupSequence, + int subtableSequence, int subtableFormat) { + if (seEntries != null) { + if ((tableType == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION) || (seEntries.size() > 0)) { + if (seSubtables != null) { + Integer tt = Integer.valueOf(tableType); + Integer lt = Integer.valueOf(lookupType); + Integer ln = Integer.valueOf(lookupSequence); + Integer lf = Integer.valueOf(lookupFlags); + Integer sn = Integer.valueOf(subtableSequence); + Integer sf = Integer.valueOf(subtableFormat); + seSubtables.add(new Object[]{tt, lt, ln, lf, sn, sf, seMapping, seEntries}); + } + } + } + } + + private void resetATSubState() { + seMapping = null; + seEntries = null; + } + + private void resetATStateAll() { + resetATState(); + gdef = null; + gsub = null; + gpos = null; + } + + /** + * helper method for formatting an integer array for output + */ + private String toString(int[] ia) { + StringBuffer sb = new StringBuffer(); + if ((ia == null) || (ia.length == 0)) { + sb.append('-'); + } else { + boolean first = true; + for (int i = 0; i < ia.length; i++) { + if (!first) { + sb.append(' '); + } else { + first = false; + } + sb.append(ia[i]); + } + } + return sb.toString(); + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFLanguage.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFLanguage.java new file mode 100644 index 0000000..87b148a --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFLanguage.java @@ -0,0 +1,433 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.fonts; + +/** + *

Language system tags defined by OTF specification. Note that this set and their + * values do not correspond with ISO639* or any other language registry.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public final class OTFLanguage { + + public static final String ABAZA = "ABA"; + public static final String ABKHAZIAN = "ABK"; + public static final String ADYGHE = "ADY"; + public static final String AFRIKAANS = "AFK"; + public static final String AFAR = "AFR"; + public static final String AGAW = "AGW"; + public static final String ALSATIAN = "ALS"; + public static final String ALTAI = "ALT"; + public static final String AMHARIC = "AMH"; + public static final String PHONETIC_AMERICANIST = "APPH"; + public static final String ARABIC = "ARA"; + public static final String AARI = "ARI"; + public static final String ARAKANESE = "ARK"; + public static final String ASSAMESE = "ASM"; + public static final String ATHAPASKAN = "ATH"; + public static final String AVAR = "AVR"; + public static final String AWADHI = "AWA"; + public static final String AYMARA = "AYM"; + public static final String AZERI = "AZE"; + public static final String BADAGA = "BAD"; + public static final String BAGHELKHANDI = "BAG"; + public static final String BALKAR = "BAL"; + public static final String BAULE = "BAU"; + public static final String BERBER = "BBR"; + public static final String BENCH = "BCH"; + public static final String BIBLE_CREE = "BCR"; + public static final String BELARUSSIAN = "BEL"; + public static final String BEMBA = "BEM"; + public static final String BENGALI = "BEN"; + public static final String BULGARIAN = "BGR"; + public static final String BHILI = "BHI"; + public static final String BHOJPURI = "BHO"; + public static final String BIKOL = "BIK"; + public static final String BILEN = "BIL"; + public static final String BLACKFOOT = "BKF"; + public static final String BALOCHI = "BLI"; + public static final String BALANTE = "BLN"; + public static final String BALTI = "BLT"; + public static final String BAMBARA = "BMB"; + public static final String BAMILEKE = "BML"; + public static final String BOSNIAN = "BOS"; + public static final String BRETON = "BRE"; + public static final String BRAHUI = "BRH"; + public static final String BRAJ_BHASHA = "BRI"; + public static final String BURMESE = "BRM"; + public static final String BASHKIR = "BSH"; + public static final String BETI = "BTI"; + public static final String CATALAN = "CAT"; + public static final String CEBUANO = "CEB"; + public static final String CHECHEN = "CHE"; + public static final String CHAHA_GURAGE = "CHG"; + public static final String CHATTISGARHI = "CHH"; + public static final String CHICHEWA = "CHI"; + public static final String CHUKCHI = "CHK"; + public static final String CHIPEWYAN = "CHP"; + public static final String CHEROKEE = "CHR"; + public static final String CHUVASH = "CHU"; + public static final String COMORIAN = "CMR"; + public static final String COPTIC = "COP"; + public static final String CORSICAN = "COS"; + public static final String CREE = "CRE"; + public static final String CARRIER = "CRR"; + public static final String CRIMEAN_TATAR = "CRT"; + public static final String CHURCH_SLAVONIC = "CSL"; + public static final String CZECH = "CSY"; + public static final String DANISH = "DAN"; + public static final String DARGWA = "DAR"; + public static final String WOODS_CREE = "DCR"; + public static final String GERMAN = "DEU"; + public static final String DEFAULT = "dflt"; + public static final String DOGRI = "DGR"; + public static final String DHIVEHI_DEPRECATED = "DHV"; + public static final String DHIVEHI = "DIV"; + public static final String DJERMA = "DJR"; + public static final String DANGME = "DNG"; + public static final String DINKA = "DNK"; + public static final String DARI = "DRI"; + public static final String DUNGAN = "DUN"; + public static final String DZONGKHA = "DZN"; + public static final String EBIRA = "EBI"; + public static final String EASTERN_CREE = "ECR"; + public static final String EDO = "EDO"; + public static final String EFIK = "EFI"; + public static final String GREEK = "ELL"; + public static final String ENGLISH = "ENG"; + public static final String ERZYA = "ERZ"; + public static final String SPANISH = "ESP"; + public static final String ESTONIAN = "ETI"; + public static final String BASQUE = "EUQ"; + public static final String EVENKI = "EVK"; + public static final String EVEN = "EVN"; + public static final String EWE = "EWE"; + public static final String FRENCH_ANTILLEAN = "FAN"; + public static final String FARSI = "FAR"; + public static final String FINNISH = "FIN"; + public static final String FIJIAN = "FJI"; + public static final String FLEMISH = "FLE"; + public static final String FOREST_NENETS = "FNE"; + public static final String FON = "FON"; + public static final String FAROESE = "FOS"; + public static final String FRENCH = "FRA"; + public static final String FRISIAN = "FRI"; + public static final String FRIULIAN = "FRL"; + public static final String FUTA = "FTA"; + public static final String FULANI = "FUL"; + public static final String GA = "GAD"; + public static final String GAELIC = "GAE"; + public static final String GAGAUZ = "GAG"; + public static final String GALICIAN = "GAL"; + public static final String GARSHUNI = "GAR"; + public static final String GARHWALI = "GAW"; + public static final String GEEZ = "GEZ"; + public static final String GILYAK = "GIL"; + public static final String GUMUZ = "GMZ"; + public static final String GONDI = "GON"; + public static final String GREENLANDIC = "GRN"; + public static final String GARO = "GRO"; + public static final String GUARANI = "GUA"; + public static final String GUJARATI = "GUJ"; + public static final String HAITIAN = "HAI"; + public static final String HALAM = "HAL"; + public static final String HARAUTI = "HAR"; + public static final String HAUSA = "HAU"; + public static final String HAWAIIN = "HAW"; + public static final String HAMMER_BANNA = "HBN"; + public static final String HILIGAYNON = "HIL"; + public static final String HINDI = "HIN"; + public static final String HIGH_MARI = "HMA"; + public static final String HINDKO = "HND"; + public static final String HO = "HO"; + public static final String HARARI = "HRI"; + public static final String CROATIAN = "HRV"; + public static final String HUNGARIAN = "HUN"; + public static final String ARMENIAN = "HYE"; + public static final String IGBO = "IBO"; + public static final String IJO = "IJO"; + public static final String ILOKANO = "ILO"; + public static final String INDONESIAN = "IND"; + public static final String INGUSH = "ING"; + public static final String INUKTITUT = "INU"; + public static final String PHONETIC_IPA = "IPPH"; + public static final String IRISH = "IRI"; + public static final String IRISH_TRADITIONAL = "IRT"; + public static final String ICELANDIC = "ISL"; + public static final String INARI_SAMI = "ISM"; + public static final String ITALIAN = "ITA"; + public static final String HEBREW = "IWR"; + public static final String JAVANESE = "JAV"; + public static final String YIDDISH = "JII"; + public static final String JAPANESE = "JAN"; + public static final String JUDEZMO = "JUD"; + public static final String JULA = "JUL"; + public static final String KABARDIAN = "KAB"; + public static final String KACHCHI = "KAC"; + public static final String KALENJIN = "KAL"; + public static final String KANNADA = "KAN"; + public static final String KARACHAY = "KAR"; + public static final String GEORGIAN = "KAT"; + public static final String KAZAKH = "KAZ"; + public static final String KEBENA = "KEB"; + public static final String KHUTSURI_GEORGIAN = "KGE"; + public static final String KHAKASS = "KHA"; + public static final String KHANTY_KAZIM = "KHK"; + public static final String KHMER = "KHM"; + public static final String KHANTY_SHURISHKAR = "KHS"; + public static final String KHANTY_VAKHI = "KHV"; + public static final String KHOWAR = "KHW"; + public static final String KIKUYU = "KIK"; + public static final String KIRGHIZ = "KIR"; + public static final String KISII = "KIS"; + public static final String KOKNI = "KKN"; + public static final String KALMYK = "KLM"; + public static final String KAMBA = "KMB"; + public static final String KUMAONI = "KMN"; + public static final String KOMO = "KMO"; + public static final String KOMSO = "KMS"; + public static final String KANURI = "KNR"; + public static final String KODAGU = "KOD"; + public static final String KOREAN_OLD_HANGUL = "KOH"; + public static final String KONKANI = "KOK"; + public static final String KIKONGO = "KON"; + public static final String KOMI_PERMYAK = "KOP"; + public static final String KOREAN = "KOR"; + public static final String KOMI_ZYRIAN = "KOZ"; + public static final String KPELLE = "KPL"; + public static final String KRIO = "KRI"; + public static final String KARAKALPAK = "KRK"; + public static final String KARELIAN = "KRL"; + public static final String KARAIM = "KRM"; + public static final String KAREN = "KRN"; + public static final String KOORETE = "KRT"; + public static final String KASHMIRI = "KSH"; + public static final String KHASI = "KSI"; + public static final String KILDIN_SAMI = "KSM"; + public static final String KUI = "KUI"; + public static final String KULVI = "KUL"; + public static final String KUMYK = "KUM"; + public static final String KURDISH = "KUR"; + public static final String KURUKH = "KUU"; + public static final String KUY = "KUY"; + public static final String KORYAK = "KYK"; + public static final String LADIN = "LAD"; + public static final String LAHULI = "LAH"; + public static final String LAK = "LAK"; + public static final String LAMBANI = "LAM"; + public static final String LAO = "LAO"; + public static final String LATIN = "LAT"; + public static final String LAZ = "LAZ"; + public static final String L_CREE = "LCR"; + public static final String LADAKHI = "LDK"; + public static final String LEZGI = "LEZ"; + public static final String LINGALA = "LIN"; + public static final String LOW_MARI = "LMA"; + public static final String LIMBU = "LMB"; + public static final String LOMWE = "LMW"; + public static final String LOWER_SORBIAN = "LSB"; + public static final String LULE_SAMI = "LSM"; + public static final String LITHUANIAN = "LTH"; + public static final String LUXEMBOURGISH = "LTZ"; + public static final String LUBA = "LUB"; + public static final String LUGANDA = "LUG"; + public static final String LUHYA = "LUH"; + public static final String LUO = "LUO"; + public static final String LATVIAN = "LVI"; + public static final String MAJANG = "MAJ"; + public static final String MAKUA = "MAK"; + public static final String MALAYALAM_TRADITIONAL = "MAL"; + public static final String MANSI = "MAN"; + public static final String MAPUDUNGUN = "MAP"; + public static final String MARATHI = "MAR"; + public static final String MARWARI = "MAW"; + public static final String MBUNDU = "MBN"; + public static final String MANCHU = "MCH"; + public static final String MOOSE_CREE = "MCR"; + public static final String MENDE = "MDE"; + public static final String MEEN = "MEN"; + public static final String MIZO = "MIZ"; + public static final String MACEDONIAN = "MKD"; + public static final String MALE = "MLE"; + public static final String MALAGASY = "MLG"; + public static final String MALINKE = "MLN"; + public static final String MALAYALAM_REFORMED = "MLR"; + public static final String MALAY = "MLY"; + public static final String MANDINKA = "MND"; + public static final String MONGOLIAN = "MNG"; + public static final String MANIPURI = "MNI"; + public static final String MANINKA = "MNK"; + public static final String MANX_GAELIC = "MNX"; + public static final String MOHAWK = "MOH"; + public static final String MOKSHA = "MOK"; + public static final String MOLDAVIAN = "MOL"; + public static final String MON = "MON"; + public static final String MOROCCAN = "MOR"; + public static final String MAORI = "MRI"; + public static final String MAITHILI = "MTH"; + public static final String MALTESE = "MTS"; + public static final String MUNDARI = "MUN"; + public static final String NAGA_ASSAMESE = "NAG"; + public static final String NANAI = "NAN"; + public static final String NASKAPI = "NAS"; + public static final String N_CREE = "NCR"; + public static final String NDEBELE = "NDB"; + public static final String NDONGA = "NDG"; + public static final String NEPALI = "NEP"; + public static final String NEWARI = "NEW"; + public static final String NAGARI = "NGR"; + public static final String NORWAY_HOUSE_CREE = "NHC"; + public static final String NISI = "NIS"; + public static final String NIUEAN = "NIU"; + public static final String NKOLE = "NKL"; + public static final String NKO = "NKO"; + public static final String DUTCH = "NLD"; + public static final String NOGAI = "NOG"; + public static final String NORWEGIAN = "NOR"; + public static final String NORTHERN_SAMI = "NSM"; + public static final String NORTHERN_TAI = "NTA"; + public static final String ESPERANTO = "NTO"; + public static final String NYNORSK = "NYN"; + public static final String OCCITAN = "OCI"; + public static final String OJI_CREE = "OCR"; + public static final String OJIBWAY = "OJB"; + public static final String ORIYA = "ORI"; + public static final String OROMO = "ORO"; + public static final String OSSETIAN = "OSS"; + public static final String PALESTINIAN_ARAMAIC = "PAA"; + public static final String PALI = "PAL"; + public static final String PUNJABI = "PAN"; + public static final String PALPA = "PAP"; + public static final String PASHTO = "PAS"; + public static final String POLYTONIC_GREEK = "PGR"; + public static final String FILIPINO = "PIL"; + public static final String PALAUNG = "PLG"; + public static final String POLISH = "PLK"; + public static final String PROVENCAL = "PRO"; + public static final String PORTUGUESE = "PTG"; + public static final String CHIN = "QIN"; + public static final String RAJASTHANI = "RAJ"; + public static final String R_CREE = "RCR"; + public static final String RUSSIAN_BURIAT = "RBU"; + public static final String RIANG = "RIA"; + public static final String RHAETO_ROMANIC = "RMS"; + public static final String ROMANIAN = "ROM"; + public static final String ROMANY = "ROY"; + public static final String RUSYN = "RSY"; + public static final String RUANDA = "RUA"; + public static final String RUSSIAN = "RUS"; + public static final String SADRI = "SAD"; + public static final String SANSKRIT = "SAN"; + public static final String SANTALI = "SAT"; + public static final String SAYISI = "SAY"; + public static final String SEKOTA = "SEK"; + public static final String SELKUP = "SEL"; + public static final String SANGO = "SGO"; + public static final String SHAN = "SHN"; + public static final String SIBE = "SIB"; + public static final String SIDAMO = "SID"; + public static final String SILTE_GURAGE = "SIG"; + public static final String SKOLT_SAMI = "SKS"; + public static final String SLOVAK = "SKY"; + public static final String SLAVEY = "SLA"; + public static final String SLOVENIAN = "SLV"; + public static final String SOMALI = "SML"; + public static final String SAMOAN = "SMO"; + public static final String SENA = "SNA"; + public static final String SINDHI = "SND"; + public static final String SINHALESE = "SNH"; + public static final String SONINKE = "SNK"; + public static final String SODO_GURAGE = "SOG"; + public static final String SOTHO = "SOT"; + public static final String ALBANIAN = "SQI"; + public static final String SERBIAN = "SRB"; + public static final String SARAIKI = "SRK"; + public static final String SERER = "SRR"; + public static final String SOUTH_SLAVEY = "SSL"; + public static final String SOUTHERN_SAMI = "SSM"; + public static final String SURI = "SUR"; + public static final String SVAN = "SVA"; + public static final String SWEDISH = "SVE"; + public static final String SWADAYA_ARAMAIC = "SWA"; + public static final String SWAHILI = "SWK"; + public static final String SWAZI = "SWZ"; + public static final String SUTU = "SXT"; + public static final String SYRIAC = "SYR"; + public static final String TABASARAN = "TAB"; + public static final String TAJIKI = "TAJ"; + public static final String TAMIL = "TAM"; + public static final String TATAR = "TAT"; + public static final String TH_CREE = "TCR"; + public static final String TELUGU = "TEL"; + public static final String TONGAN = "TGN"; + public static final String TIGRE = "TGR"; + public static final String TIGRINYA = "TGY"; + public static final String THAI = "THA"; + public static final String TAHITIAN = "THT"; + public static final String TIBETAN = "TIB"; + public static final String TURKMEN = "TKM"; + public static final String TEMNE = "TMN"; + public static final String TSWANA = "TNA"; + public static final String TUNDRA_NENETS = "TNE"; + public static final String TONGA = "TNG"; + public static final String TODO = "TOD"; + public static final String TURKISH = "TRK"; + public static final String TSONGA = "TSG"; + public static final String TUROYO_ARAMAIC = "TUA"; + public static final String TULU = "TUL"; + public static final String TUVIN = "TUV"; + public static final String TWI = "TWI"; + public static final String UDMURT = "UDM"; + public static final String UKRAINIAN = "UKR"; + public static final String URDU = "URD"; + public static final String UPPER_SORBIAN = "USB"; + public static final String UYGHUR = "UYG"; + public static final String UZBEK = "UZB"; + public static final String VENDA = "VEN"; + public static final String VIETNAMESE = "VIT"; + public static final String WA = "WA"; + public static final String WAGDI = "WAG"; + public static final String WEST_CREE = "WCR"; + public static final String WELSH = "WEL"; + public static final String WILDCARD = "*"; + public static final String WOLOF = "WLF"; + public static final String TAI_LUE = "XBD"; + public static final String XHOSA = "XHS"; + public static final String SAKHA = "YAK"; + public static final String YORUBA = "YBA"; + public static final String Y_CREE = "YCR"; + public static final String YI_CLASSIC = "YIC"; + public static final String YI_MODERN = "YIM"; + public static final String CHINESE_HONG_KONG_SAR = "ZHH"; + public static final String CHINESE_PHONETIC = "ZHP"; + public static final String CHINESE_SIMPLIFIED = "ZHS"; + public static final String CHINESE_TRADITIONAL = "ZHT"; + public static final String ZANDE = "ZND"; + public static final String ZULU = "ZUL"; + + public static boolean isDefault(String language) { + return (language != null) && language.equals(DEFAULT); + } + + public static boolean isWildCard(String language) { + return (language != null) && language.equals(WILDCARD); + } + + private OTFLanguage() { + } +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFScript.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFScript.java new file mode 100644 index 0000000..7b4989a --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/OTFScript.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.fonts; + +/** + *

Script tags defined by OTF specification. Note that this set and their + * values do not correspond with ISO 15924 or Unicode Script names.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public final class OTFScript { + + public static final String ARABIC = "arab"; + public static final String ARMENIAN = "armn"; + public static final String AVESTAN = "avst"; + public static final String BALINESE = "bali"; + public static final String BAMUM = "bamu"; + public static final String BATAK = "batk"; + public static final String BENGALI = "beng"; + public static final String BENGALI_V2 = "bng2"; + public static final String BOPOMOFO = "bopo"; + public static final String BRAILLE = "brai"; + public static final String BRAHMI = "brah"; + public static final String BUGINESE = "bugi"; + public static final String BUHID = "buhd"; + public static final String BYZANTINE_MUSIC = "byzm"; + public static final String CANADIAN_SYLLABICS = "cans"; + public static final String CARIAN = "cari"; + public static final String CHAKMA = "cakm"; + public static final String CHAM = "cham"; + public static final String CHEROKEE = "cher"; + public static final String CJK_IDEOGRAPHIC = "hani"; + public static final String COPTIC = "copt"; + public static final String CYPRIOT_SYLLABARY = "cprt"; + public static final String CYRILLIC = "cyrl"; + public static final String DEFAULT = "DFLT"; + public static final String DESERET = "dsrt"; + public static final String DEVANAGARI = "deva"; + public static final String DEVANAGARI_V2 = "dev2"; + public static final String EGYPTIAN_HEIROGLYPHS = "egyp"; + public static final String ETHIOPIC = "ethi"; + public static final String GEORGIAN = "geor"; + public static final String GLAGOLITIC = "glag"; + public static final String GOTHIC = "goth"; + public static final String GREEK = "grek"; + public static final String GUJARATI = "gujr"; + public static final String GUJARATI_V2 = "gjr2"; + public static final String GURMUKHI = "guru"; + public static final String GURMUKHI_V2 = "gur2"; + public static final String HANGUL = "hang"; + public static final String HANGUL_JAMO = "jamo"; + public static final String HANUNOO = "hano"; + public static final String HEBREW = "hebr"; + public static final String HIRAGANA = "kana"; + public static final String IMPERIAL_ARAMAIC = "armi"; + public static final String INSCRIPTIONAL_PAHLAVI = "phli"; + public static final String INSCRIPTIONAL_PARTHIAN = "prti"; + public static final String JAVANESE = "java"; + public static final String KAITHI = "kthi"; + public static final String KANNADA = "knda"; + public static final String KANNADA_V2 = "knd2"; + public static final String KATAKANA = "kana"; + public static final String KAYAH_LI = "kali"; + public static final String KHAROSTHI = "khar"; + public static final String KHMER = "khmr"; + public static final String LAO = "lao"; + public static final String LATIN = "latn"; + public static final String LEPCHA = "lepc"; + public static final String LIMBU = "limb"; + public static final String LINEAR_B = "linb"; + public static final String LISU = "lisu"; + public static final String LYCIAN = "lyci"; + public static final String LYDIAN = "lydi"; + public static final String MALAYALAM = "mlym"; + public static final String MALAYALAM_V2 = "mlm2"; + public static final String MANDAIC = "mand"; + public static final String MATHEMATICAL_ALPHANUMERIC_SYMBOLS = "math"; + public static final String MEITEI = "mtei"; + public static final String MEROITIC_CURSIVE = "merc"; + public static final String MEROITIC_HIEROGLYPHS = "mero"; + public static final String MONGOLIAN = "mong"; + public static final String MUSICAL_SYMBOLS = "musc"; + public static final String MYANMAR = "mymr"; + public static final String NEW_TAI_LUE = "talu"; + public static final String NKO = "nko"; + public static final String OGHAM = "ogam"; + public static final String OL_CHIKI = "olck"; + public static final String OLD_ITALIC = "ital"; + public static final String OLD_PERSIAN_CUNEIFORM = "xpeo"; + public static final String OLD_SOUTH_ARABIAN = "sarb"; + public static final String OLD_TURKIC = "orkh"; + public static final String ORIYA = "orya"; + public static final String ORIYA_V2 = "ory2"; + public static final String OSMANYA = "osma"; + public static final String PHAGS_PA = "phag"; + public static final String PHOENICIAN = "phnx"; + public static final String REJANG = "rjng"; + public static final String RUNIC = "runr"; + public static final String SAMARITAN = "samr"; + public static final String SAURASHTRA = "saur"; + public static final String SHARADA = "shrd"; + public static final String SHAVIAN = "shaw"; + public static final String SINHALA = "sinh"; + public static final String SORA_SOMPENG = "sora"; + public static final String SUMERO_AKKADIAN_CUNEIFORM = "xsux"; + public static final String SUNDANESE = "sund"; + public static final String SYLOTI_NAGRI = "sylo"; + public static final String SYRIAC = "syrc"; + public static final String TAGALOG = "tglg"; + public static final String TAGBANWA = "tagb"; + public static final String TAI_LE = "tale"; + public static final String TAI_THAM = "lana"; + public static final String TAI_VIET = "tavt"; + public static final String TAKRI = "takr"; + public static final String TAMIL = "taml"; + public static final String TAMIL_V2 = "tml2"; + public static final String TELUGU = "telu"; + public static final String TELUGU_V2 = "tel2"; + public static final String THAANA = "thaa"; + public static final String THAI = "thai"; + public static final String TIBETAN = "tibt"; + public static final String TIFINAGH = "tfng"; + public static final String UGARITIC_CUNEIFORM = "ugar"; + public static final String VAI = "vai"; + public static final String WILDCARD = "*"; + public static final String YI = "yi"; + + public static boolean isDefault(String script) { + return (script != null) && script.equals(DEFAULT); + } + + public static boolean isWildCard(String script) { + return (script != null) && script.equals(DEFAULT); + } + + private OTFScript() { + } +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/SingleByteEncoding.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/SingleByteEncoding.java new file mode 100644 index 0000000..7a41fda --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/SingleByteEncoding.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.fonts; + +/** + * The interface defines a 1-byte character encoding (with 256 characters). + */ +public interface SingleByteEncoding { + + /** + * Code point that is used if no code point for a specific character has been found. + */ + char NOT_FOUND_CODE_POINT = '\0'; + + /** + * Returns the encoding's name. + * + * @return the name of the encoding + */ + String getName(); + + /** + * Maps a Unicode character to a code point in the encoding. + * + * @param c the Unicode character to map + * @return the code point in the encoding or 0 (=.notdef) if not found + */ + char mapChar(char c); + + /** + * Returns the array of character names for this encoding. + * + * @return the array of character names + * (unmapped code points are represented by a ".notdef" value) + */ + String[] getCharNameMap(); + + /** + * Returns a character array with Unicode scalar values which can be used to map encoding + * code points to Unicode values. Note that this does not return all possible Unicode values + * that the encoding maps. + * + * @return a character array with Unicode scalar values + */ + char[] getUnicodeCharMap(); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Typeface.java b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Typeface.java new file mode 100644 index 0000000..21e6427 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/fonts/Typeface.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 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. + * + */ + +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 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(); + } + if (warnedChars.size() < 8 && !warnedChars.contains(ch)) { + warnedChars.add(ch); + } + } + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuffer sbuf = new StringBuffer(super.toString()); + sbuf.append('{'); + sbuf.append(getFullName()); + sbuf.append('}'); + return sbuf.toString(); + } +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/ByteArrayOutputStream.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/ByteArrayOutputStream.java new file mode 100644 index 0000000..7c43749 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/io/ByteArrayOutputStream.java @@ -0,0 +1,429 @@ +/* + * Copyright (C) 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. + * + */ + +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. + *

+ * The data can be retrieved using toByteArray() and + * toString(). + *

+ * 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}. + *

+ * 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 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 InputStream and represent + * same data as result InputStream. + *

+ * This method is useful where, + *

    + *
  • Source InputStream is slow.
  • + *
  • It has network resources associated, so we cannot keep it open for + * long time.
  • + *
  • It has network timeout associated.
  • + *
+ * It can be used in favor of {@link #toByteArray()}, since it + * avoids unnecessary allocation and copy of byte[].
+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @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 InputStream and represent + * same data as result InputStream. + *

+ * This method is useful where, + *

    + *
  • Source InputStream is slow.
  • + *
  • It has network resources associated, so we cannot keep it open for + * long time.
  • + *
  • It has network timeout associated.
  • + *
+ * It can be used in favor of {@link #toByteArray()}, since it + * avoids unnecessary allocation and copy of byte[].
+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @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 this stream, + * avoiding memory allocation and copy, thus saving space and time.
+ * + * @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 list = new ArrayList(buffers.size()); + for (final byte[] buf : buffers) { + final int c = Math.min(buf.length, remaining); + list.add(new ByteArrayInputStream(buf, 0, c)); + remaining -= c; + if (remaining == 0) { + break; + } + } + reuseBuffers = false; + return new SequenceInputStream(Collections.enumeration(list)); + } + + /** + * Gets the curent contents of this byte stream as a byte array. + * The result is independent of this stream. + * + * @return the current contents of this output stream, as a byte array + * @see java.io.ByteArrayOutputStream#toByteArray() + */ + public synchronized byte[] toByteArray() { + int remaining = count; + if (remaining == 0) { + return EMPTY_BYTE_ARRAY; + } + final byte newbuf[] = new byte[remaining]; + int pos = 0; + for (final byte[] buf : buffers) { + final int c = Math.min(buf.length, remaining); + System.arraycopy(buf, 0, newbuf, pos, c); + pos += c; + remaining -= c; + if (remaining == 0) { + break; + } + } + return newbuf; + } + + /** + * Gets the curent contents of this byte stream as a string + * using the platform default charset. + * + * @return the contents of the byte array as a String + * @see java.io.ByteArrayOutputStream#toString() + * @deprecated 2.5 use {@link #toString(String)} instead + */ + @Override + @Deprecated + public String toString() { + // make explicit the use of the default charset + return new String(toByteArray(), Charset.defaultCharset()); + } + + /** + * Gets the curent contents of this byte stream as a string + * using the specified encoding. + * + * @param enc the name of the character encoding + * @return the string converted from the byte array + * @throws UnsupportedEncodingException if the encoding is not supported + * @see java.io.ByteArrayOutputStream#toString(String) + */ + public String toString(final String enc) throws UnsupportedEncodingException { + return new String(toByteArray(), enc); + } + + /** + * Gets the curent contents of this byte stream as a string + * using the specified encoding. + * + * @param charset the character encoding + * @return the string converted from the byte array + * @see java.io.ByteArrayOutputStream#toString(String) + * @since 2.5 + */ + public String toString(final Charset charset) { + return new String(toByteArray(), charset); + } + +} \ No newline at end of file diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/Charsets.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/Charsets.java new file mode 100644 index 0000000..3838375 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/io/Charsets.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 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. + * + */ + +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. + *

+ * From the Java documentation + * Standard charsets: + *

+ * 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. + *

+ * + *
    + *
  • US-ASCII
    + * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode character set.
  • + *
  • ISO-8859-1
    + * ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.
  • + *
  • UTF-8
    + * Eight-bit Unicode Transformation Format.
  • + *
  • UTF-16BE
    + * Sixteen-bit Unicode Transformation Format, big-endian byte order.
  • + *
  • UTF-16LE
    + * Sixteen-bit Unicode Transformation Format, little-endian byte order.
  • + *
  • UTF-16
    + * 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.)
  • + *
+ * + * @see Standard charsets + */ +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. + *

+ * From the Java documentation + * Standard charsets: + *

+ * + * @return An immutable, case-insensitive map from canonical charset names to charset objects. + * @see Charset#availableCharsets() + */ + public static SortedMap requiredCharsets() { + // maybe cache? + // TODO Re-implement on Java 7 to use java.nio.charset.StandardCharsets + final TreeMap m = new TreeMap(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. + *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + + /** + *

+ * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set. + *

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final Charset US_ASCII = Charset.forName("US-ASCII"); + + /** + *

+ * 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) + *

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final Charset UTF_16 = Charset.forName("UTF-16"); + + /** + *

+ * Sixteen-bit Unicode Transformation Format, big-endian byte order. + *

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final Charset UTF_16BE = Charset.forName("UTF-16BE"); + + /** + *

+ * Sixteen-bit Unicode Transformation Format, little-endian byte order. + *

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final Charset UTF_16LE = Charset.forName("UTF-16LE"); + + /** + *

+ * Eight-bit Unicode Transformation Format. + *

+ *

+ * Every implementation of the Java platform is required to support this character encoding. + *

+ * + * @see Standard charsets + */ + public static final Charset UTF_8 = Charset.forName("UTF-8"); + +} \ No newline at end of file diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/ClosedInputStream.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/ClosedInputStream.java new file mode 100644 index 0000000..c268bde --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/io/ClosedInputStream.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 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. + * + */ + +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. + *

+ * Typically uses of this class include testing for corner cases in methods + * that accept input streams and acting as a sentinel value instead of a + * {@code null} input stream. + */ +public class ClosedInputStream extends InputStream { + + /** + * A singleton. + */ + public static final ClosedInputStream CLOSED_INPUT_STREAM = new ClosedInputStream(); + + /** + * Returns -1 to indicate that the stream is closed. + * + * @return always -1 + */ + @Override + public int read() { + return EOF; + } + +} \ No newline at end of file diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/IOUtils.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/IOUtils.java new file mode 100644 index 0000000..42a51e3 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/io/IOUtils.java @@ -0,0 +1,3051 @@ +/* + * Copyright (C) 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. + * + */ + +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. + *

+ * This class provides static utility methods for input/output operations. + *

    + *
  • closeQuietly - these methods close a stream ignoring nulls and exceptions + *
  • toXxx/read - these methods read data from a stream + *
  • write - these methods write data to a stream + *
  • copy - these methods copy all the data from one stream to another + *
  • contentEquals - these methods compare the content of two streams + *
+ *

+ * 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. + *

+ * All the methods in this class that read a stream are buffered internally. + * This means that there is no cause to use a BufferedInputStream + * or BufferedReader. The default buffer size of 4K has been shown + * to be efficient in tests. + *

+ * The various copy methods all delegate the actual copying to one of the following methods: + *

    + *
  • {@link #copyLarge(InputStream, OutputStream, byte[])}
  • + *
  • {@link #copyLarge(InputStream, OutputStream, long, long, byte[])}
  • + *
  • {@link #copyLarge(Reader, Writer, char[])}
  • + *
  • {@link #copyLarge(Reader, Writer, long, long, char[])}
  • + *
+ * 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[])}. + *

+ * 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. + *

+ * Wherever possible, the methods in this class do not 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. + *

+ * 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 Reader unconditionally. + *

+ * Equivalent to {@link Reader#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+	 *   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);
+	 *   }
+	 * 
+ * + * @param input the Reader to close, may be null or already closed + */ + public static void closeQuietly(final Reader input) { + closeQuietly((Closeable) input); + } + + /** + * Closes an Writer unconditionally. + *

+ * Equivalent to {@link Writer#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+	 *   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);
+	 *   }
+	 * 
+ * + * @param output the Writer to close, may be null or already closed + */ + public static void closeQuietly(final Writer output) { + closeQuietly((Closeable) output); + } + + /** + * Closes an InputStream unconditionally. + *

+ * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+	 *   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);
+	 *   }
+	 * 
+ * + * @param input the InputStream to close, may be null or already closed + */ + public static void closeQuietly(final InputStream input) { + closeQuietly((Closeable) input); + } + + /** + * Closes an OutputStream unconditionally. + *

+ * Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+	 * 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);
+	 * }
+	 * 
+ * + * @param output the OutputStream to close, may be null or already closed + */ + public static void closeQuietly(final OutputStream output) { + closeQuietly((Closeable) output); + } + + /** + * Closes a Closeable unconditionally. + *

+ * Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. This is typically used in + * finally blocks. + *

+ * Example code: + *

+ *
+	 * Closeable closeable = null;
+	 * try {
+	 *     closeable = new FileReader("foo.txt");
+	 *     // process closeable
+	 *     closeable.close();
+	 * } catch (Exception e) {
+	 *     // error handling
+	 * } finally {
+	 *     IOUtils.closeQuietly(closeable);
+	 * }
+	 * 
+ *

+ * Closing all streams: + *

+ *
+	 * try {
+	 *     return IOUtils.copy(inputStream, outputStream);
+	 * } finally {
+	 *     IOUtils.closeQuietly(inputStream);
+	 *     IOUtils.closeQuietly(outputStream);
+	 * }
+	 * 
+ * + * @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 Closeable unconditionally. + *

+ * Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. + *

+ * 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. + *
+ * It should not be used to replace the close statement(s) + * which should be present for the non-exceptional case. + *
+ * 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. + *

+ * Example code: + *

+ *
+	 * 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 {
+	 *     IOUtils.closeQuietly(closeable); // In case normal close was skipped due to Exception
+	 * }
+	 * 
+ *

+ * Closing all streams: + *
+ *

+	 * try {
+	 *     return IOUtils.copy(inputStream, outputStream);
+	 * } finally {
+	 *     IOUtils.closeQuietly(inputStream, outputStream);
+	 * }
+	 * 
+ * + * @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 Socket unconditionally. + *

+ * Equivalent to {@link Socket#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+	 *   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);
+	 *   }
+	 * 
+ * + * @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 Selector unconditionally. + *

+ * Equivalent to {@link Selector#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+	 *   Selector selector = null;
+	 *   try {
+	 *       selector = Selector.open();
+	 *       // process socket
+	 *
+	 *   } catch (Exception e) {
+	 *       // error handling
+	 *   } finally {
+	 *       IOUtils.closeQuietly(selector);
+	 *   }
+	 * 
+ * + * @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 ServerSocket unconditionally. + *

+ * Equivalent to {@link ServerSocket#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+	 *   ServerSocket socket = null;
+	 *   try {
+	 *       socket = new ServerSocket();
+	 *       // process socket
+	 *       socket.close();
+	 *   } catch (Exception e) {
+	 *       // error handling
+	 *   } finally {
+	 *       IOUtils.closeQuietly(socket);
+	 *   }
+	 * 
+ * + * @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 InputStream and represent + * same data as result InputStream. + *

+ * This method is useful where, + *

    + *
  • Source InputStream is slow.
  • + *
  • It has network resources associated, so we cannot keep it open for + * long time.
  • + *
  • It has network timeout associated.
  • + *
+ * It can be used in favor of {@link #toByteArray(InputStream)}, since it + * avoids unnecessary allocation and copy of byte[].
+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @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 InputStream and represent + * same data as result InputStream. + *

+ * This method is useful where, + *

    + *
  • Source InputStream is slow.
  • + *
  • It has network resources associated, so we cannot keep it open for + * long time.
  • + *
  • It has network timeout associated.
  • + *
+ * It can be used in favor of {@link #toByteArray(InputStream)}, since it + * avoids unnecessary allocation and copy of byte[].
+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @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 InputStream as a byte[]. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream 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 InputStream as a byte[]. + * Use this method instead of toByteArray(InputStream) + * when InputStream size is known. + * NOTE: 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 InputStream to read from + * @param size the size of InputStream + * @return the requested byte array + * @throws IOException if an I/O error occurs or InputStream 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 InputStream as a byte[]. + * Use this method instead of toByteArray(InputStream) + * when InputStream size is known + * + * @param input the InputStream to read from + * @param size the size of InputStream + * @return the requested byte array + * @throws IOException if an I/O error occurs or InputStream 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 Reader as a byte[] + * using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader 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 Reader as a byte[] + * using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader 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 Reader as a byte[] + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader 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 String as a byte[] + * using the default character encoding of the platform. + *

+ * This is the same as {@link String#getBytes()}. + * + * @param input the String 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 URI as a byte[]. + * + * @param uri the URI 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 URL as a byte[]. + * + * @param url the URL 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 URLConnection as a byte[]. + * + * @param urlConn the URLConnection 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 InputStream as a character array + * using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param is the InputStream 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 InputStream as a character array + * using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param is the InputStream 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 InputStream as a character array + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param is the InputStream 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 Reader as a character array. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader 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 InputStream as a String + * using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream 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 InputStream as a String + * using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * + * @param input the InputStream 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 InputStream as a String + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream 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 Reader as a String. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader 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 byte[] 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 byte[] as a String + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + * + * @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 InputStream as a list of Strings, + * one entry per line, using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream 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 readLines(final InputStream input) throws IOException { + return readLines(input, Charset.defaultCharset()); + } + + /** + * Gets the contents of an InputStream as a list of Strings, + * one entry per line, using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream 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 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 InputStream as a list of Strings, + * one entry per line, using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream 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 readLines(final InputStream input, final String encoding) throws IOException { + return readLines(input, Charsets.toCharset(encoding)); + } + + /** + * Gets the contents of a Reader as a list of Strings, + * one entry per line. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader 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 readLines(final Reader input) throws IOException { + final BufferedReader reader = toBufferedReader(input); + final List list = new ArrayList(); + String line = reader.readLine(); + while (line != null) { + list.add(line); + line = reader.readLine(); + } + return list; + } + + // lineIterator + //----------------------------------------------------------------------- + + /** + * Returns an Iterator for the lines in a Reader. + *

+ * LineIterator holds a reference to the open + * Reader 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)}. + *

+ * The recommended usage pattern is: + *

+	 * try {
+	 *   LineIterator it = IOUtils.lineIterator(reader);
+	 *   while (it.hasNext()) {
+	 *     String line = it.nextLine();
+	 *     /// do something with line
+	 *   }
+	 * } finally {
+	 *   IOUtils.closeQuietly(reader);
+	 * }
+	 * 
+ * + * @param reader the Reader 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 InputStream, using + * the character encoding specified (or default encoding if null). + *

+ * LineIterator holds a reference to the open + * InputStream 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)}. + *

+ * The recommended usage pattern is: + *

+	 * try {
+	 *   LineIterator it = IOUtils.lineIterator(stream, charset);
+	 *   while (it.hasNext()) {
+	 *     String line = it.nextLine();
+	 *     /// do something with line
+	 *   }
+	 * } finally {
+	 *   IOUtils.closeQuietly(stream);
+	 * }
+	 * 
+ * + * @param input the InputStream 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 InputStream, using + * the character encoding specified (or default encoding if null). + *

+ * LineIterator holds a reference to the open + * InputStream 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)}. + *

+ * The recommended usage pattern is: + *

+	 * try {
+	 *   LineIterator it = IOUtils.lineIterator(stream, "UTF-8");
+	 *   while (it.hasNext()) {
+	 *     String line = it.nextLine();
+	 *     /// do something with line
+	 *   }
+	 * } finally {
+	 *   IOUtils.closeQuietly(stream);
+	 * }
+	 * 
+ * + * @param input the InputStream 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. + *

+ * Character encoding names can be found at + * IANA. + * + * @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. + *

+ * Character encoding names can be found at + * IANA. + * + * @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 byte[] to an OutputStream. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the OutputStream 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 byte[] to an OutputStream 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 OutputStream 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 byte[] to chars on a Writer + * using the default character encoding of the platform. + *

+ * This method uses {@link String#String(byte[])}. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the Writer 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 byte[] to chars on a Writer + * using the specified character encoding. + *

+ * 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 Writer 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 byte[] to chars on a Writer + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * 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 Writer 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 char[] to a Writer + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the Writer 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 char[] to a Writer 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 Writer 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 char[] to bytes on an + * OutputStream. + *

+ * 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 OutputStream 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 char[] to bytes on an + * OutputStream using the specified character encoding. + *

+ * 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 OutputStream 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 char[] to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * 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 OutputStream 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 CharSequence to a Writer. + * + * @param data the CharSequence to write, null ignored + * @param output the Writer 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 CharSequence to bytes on an + * OutputStream using the default character encoding of the + * platform. + *

+ * This method uses {@link String#getBytes()}. + * + * @param data the CharSequence to write, null ignored + * @param output the OutputStream 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 CharSequence to bytes on an + * OutputStream using the specified character encoding. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the CharSequence to write, null ignored + * @param output the OutputStream 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 CharSequence to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the CharSequence to write, null ignored + * @param output the OutputStream 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 String to a Writer. + * + * @param data the String to write, null ignored + * @param output the Writer 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 String to bytes on an + * OutputStream using the default character encoding of the + * platform. + *

+ * This method uses {@link String#getBytes()}. + * + * @param data the String to write, null ignored + * @param output the OutputStream 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 String to bytes on an + * OutputStream using the specified character encoding. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the String to write, null ignored + * @param output the OutputStream 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 String to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the String to write, null ignored + * @param output the OutputStream 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 StringBuffer to a Writer. + * + * @param data the StringBuffer to write, null ignored + * @param output the Writer 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 StringBuffer to bytes on an + * OutputStream using the default character encoding of the + * platform. + *

+ * This method uses {@link String#getBytes()}. + * + * @param data the StringBuffer to write, null ignored + * @param output the OutputStream 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 StringBuffer to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the StringBuffer to write, null ignored + * @param output the OutputStream 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 toString() value of each item in a collection to + * an OutputStream 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 OutputStream 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 toString() value of each item in a collection to + * an OutputStream 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 OutputStream 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 toString() value of each item in a collection to + * an OutputStream line by line, using the specified character + * encoding and the specified line ending. + *

+ * Character encoding names can be found at + * IANA. + * + * @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 OutputStream 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 toString() value of each item in a collection to + * a Writer 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 Writer 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 InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * Large streams (over 2GB) will return a bytes copied value of + * -1 after the copy has completed since the correct + * number of bytes cannot be returned as an int. For large streams + * use the copyLarge(InputStream, OutputStream) method. + * + * @param input the InputStream to read from + * @param output the OutputStream 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 InputStream to an OutputStream using an internal buffer of the + * given size. + *

+ * This method buffers the input internally, so there is no need to use a BufferedInputStream. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream 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) InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the InputStream to read from + * @param output the OutputStream 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) InputStream to an + * OutputStream. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream 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) InputStream to an + * OutputStream, optionally skipping input bytes. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ *

+ * 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. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the InputStream to read from + * @param output the OutputStream 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) InputStream to an + * OutputStream, optionally skipping input bytes. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

+ *

+ * 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. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream 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 InputStream to chars on a + * Writer using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer 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 InputStream to chars on a + * Writer using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer 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 InputStream to chars on a + * Writer using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer 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 Reader to a Writer. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Large streams (over 2GB) will return a chars copied value of + * -1 after the copy has completed since the correct + * number of chars cannot be returned as an int. For large streams + * use the copyLarge(Reader, Writer) method. + * + * @param input the Reader to read from + * @param output the Writer 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) Reader to a Writer. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the Reader to read from + * @param output the Writer 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) Reader to a Writer. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedReader. + *

+ * + * @param input the Reader to read from + * @param output the Writer 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) InputStream to an + * OutputStream, optionally skipping input chars. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the Reader to read from + * @param output the Writer 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) InputStream to an + * OutputStream, optionally skipping input chars. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedReader. + *

+ * + * @param input the Reader to read from + * @param output the Writer 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 Reader to bytes on an + * OutputStream using the default character encoding of the + * platform, and calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ * This method uses {@link OutputStreamWriter}. + * + * @param input the Reader to read from + * @param output the OutputStream 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 Reader to bytes on an + * OutputStream using the specified character encoding, and + * calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ *

+ * This method uses {@link OutputStreamWriter}. + *

+ * + * @param input the Reader to read from + * @param output the OutputStream 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 Reader to bytes on an + * OutputStream using the specified character encoding, and + * calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Character encoding names can be found at + * IANA. + *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ * This method uses {@link OutputStreamWriter}. + * + * @param input the Reader to read from + * @param output the OutputStream 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. + *

+ * This method buffers the input internally using + * BufferedInputStream 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. + *

+ * This method buffers the input internally using + * BufferedReader 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. + *

+ * This method buffers the input internally using + * BufferedReader 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}. + *

+ * 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. + *

+ * + * @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 IO-203 - Add skipFully() method for InputStreams + * @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}. + *

+ * 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. + *

+ * + * @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 IO-203 - Add skipFully() method for InputStreams + * @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. + *

+ * This allows for the possibility that {@link InputStream#skip(long)} may + * not skip as many bytes as requested (most likely because of reaching EOF). + *

+ * 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. + *

+ * + * @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. + *

+ * This allows for the possibility that {@link Reader#skip(long)} may + * not skip as many characters as requested (most likely because of reaching EOF). + *

+ * 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. + *

+ * + * @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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * This allows for the possibility that {@link ReadableByteChannel#read(ByteBuffer)} may + * not read as many bytes as requested (most likely because of reaching EOF). + * + * @param input the byte channel to read + * @param buffer byte buffer destination + * @throws IOException if there is a problem reading the file + * @throws EOFException if the number of bytes read was incorrect + * @since 2.5 + */ + public static void readFully(final ReadableByteChannel input, final ByteBuffer buffer) throws IOException { + final int expected = buffer.remaining(); + final int actual = read(input, buffer); + if (actual != expected) { + throw new EOFException("Length to read: " + expected + " actual: " + actual); + } + } + +} \ No newline at end of file diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/LineIterator.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/LineIterator.java new file mode 100644 index 0000000..e00d048 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/io/LineIterator.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 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. + * + */ + +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 Reader. + *

+ * LineIterator holds a reference to an open Reader. + * 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. + *

+ * The recommended usage pattern is: + *

+ * LineIterator it = FileUtils.lineIterator(file, "UTF-8");
+ * try {
+ *   while (it.hasNext()) {
+ *     String line = it.nextLine();
+ *     // do something with line
+ *   }
+ * } finally {
+ *   it.close();
+ * }
+ * 
+ */ +public class LineIterator implements Iterator { + + // 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 Reader. + * + * @param reader the Reader 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 Reader has more lines. + * If there is an IOException 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 Reader. + * + * @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 Reader. + * + * @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 Reader 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 Reader remains open. + * This method can safely be called multiple times. + */ + public void close() { + finished = true; + IOUtils.closeQuietly(bufferedReader); + cachedLine = null; + } + + /** + * Unsupported. + * + * @throws UnsupportedOperationException always + */ + public void remove() { + throw new UnsupportedOperationException("Remove unsupported on LineIterator"); + } + + //----------------------------------------------------------------------- + + /** + * Closes the iterator, handling null and ignoring exceptions. + * + * @param iterator the iterator to close + */ + public static void closeQuietly(final LineIterator iterator) { + if (iterator != null) { + iterator.close(); + } + } + +} \ No newline at end of file diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/io/StringBuilderWriter.java b/fontparser/src/main/java/com/jaredrummler/fontreader/io/StringBuilderWriter.java new file mode 100644 index 0000000..f19bfe9 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/io/StringBuilderWriter.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.io; + +import java.io.Serializable; +import java.io.Writer; + +/** + * {@link Writer} implementation that outputs to a {@link StringBuilder}. + *

+ * NOTE: This implementation, as an alternative to + * java.io.StringWriter, provides an un-synchronized + * (i.e. for use in a single thread) implementation for better performance. + * For safe usage with multiple {@link Thread}s then + * java.io.StringWriter 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}. + * + *

If {@code builder} is null a new instance with default capacity will be created.

+ * + * @param builder The String builder. May be null. + */ + public StringBuilderWriter(final StringBuilder builder) { + this.builder = builder != null ? builder : new StringBuilder(); + } + + /** + * Appends a single character to this Writer. + * + * @param value The character to append + * @return This writer instance + */ + @Override + public Writer append(final char value) { + builder.append(value); + return this; + } + + /** + * Appends a character sequence to this Writer. + * + * @param value The character to append + * @return This writer instance + */ + @Override + public Writer append(final CharSequence value) { + builder.append(value); + return this; + } + + /** + * Appends a portion of a character sequence to the {@link StringBuilder}. + * + * @param value The character to append + * @param start The index of the first character + * @param end The index of the last character + 1 + * @return This writer instance + */ + @Override + public Writer append(final CharSequence value, final int start, final int end) { + builder.append(value, start, end); + return this; + } + + /** + * Closing this writer has no effect. + */ + @Override + public void close() { + // no-op + } + + /** + * Flushing this writer has no effect. + */ + @Override + public void flush() { + // no-op + } + + /** + * Writes a String to the {@link StringBuilder}. + * + * @param value The value to write + */ + @Override + public void write(final String value) { + if (value != null) { + builder.append(value); + } + } + + /** + * Writes a portion of a character array to the {@link StringBuilder}. + * + * @param value The value to write + * @param offset The index of the first character + * @param length The number of characters to write + */ + @Override + public void write(final char[] value, final int offset, final int length) { + if (value != null) { + builder.append(value, offset, length); + } + } + + /** + * Returns the underlying builder. + * + * @return The underlying builder + */ + public StringBuilder getBuilder() { + return builder; + } + + /** + * Returns {@link StringBuilder#toString()}. + * + * @return The contents of the String builder. + */ + @Override + public String toString() { + return builder.toString(); + } + +} \ No newline at end of file diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/FontFileReader.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/FontFileReader.java new file mode 100644 index 0000000..d8a5207 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/FontFileReader.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.truetype; + +import com.jaredrummler.fontreader.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Reads a TrueType font file into a byte array and + * provides file like functions for array access. + */ +public class FontFileReader { + + private final int fsize; // file size + private int current; // current position in file + private final byte[] file; + + /** + * Constructor + * + * @param in InputStream to read from + * @throws IOException In case of an I/O problem + */ + public FontFileReader(InputStream in) throws IOException { + this.file = IOUtils.toByteArray(in); + this.fsize = this.file.length; + this.current = 0; + } + + /** + * Set current file position to offset + * + * @param offset The new offset to set + * @throws IOException In case of an I/O problem + */ + public void seekSet(long offset) throws IOException { + if (offset > fsize || offset < 0) { + throw new java.io.EOFException("Reached EOF, file size=" + fsize + + " offset=" + offset); + } + current = (int) offset; + } + + /** + * Skip a given number of bytes. + * + * @param add The number of bytes to advance + * @throws IOException In case of an I/O problem + */ + public void skip(long add) throws IOException { + seekSet(current + add); + } + + /** + * Returns current file position. + * + * @return int The current position. + */ + public int getCurrentPos() { + return current; + } + + /** + * Returns the size of the file. + * + * @return int The filesize + */ + public int getFileSize() { + return fsize; + } + + /** + * Read 1 byte. + * + * @return One byte + * @throws IOException If EOF is reached + */ + private byte read() throws IOException { + if (current >= fsize) { + throw new java.io.EOFException("Reached EOF, file size=" + fsize); + } + + final byte ret = file[current++]; + return ret; + } + + /** + * Read 1 signed byte. + * + * @return One byte + * @throws IOException If EOF is reached + */ + public final byte readTTFByte() throws IOException { + return read(); + } + + /** + * Read 1 unsigned byte. + * + * @return One unsigned byte + * @throws IOException If EOF is reached + */ + public final int readTTFUByte() throws IOException { + final byte buf = read(); + + if (buf < 0) { + return (256 + buf); + } else { + return buf; + } + } + + /** + * Read 2 bytes signed. + * + * @return One signed short + * @throws IOException If EOF is reached + */ + public final short readTTFShort() throws IOException { + final int ret = (readTTFUByte() << 8) + readTTFUByte(); + final short sret = (short) ret; + return sret; + } + + /** + * Read 2 bytes unsigned. + * + * @return One unsigned short + * @throws IOException If EOF is reached + */ + public final int readTTFUShort() throws IOException { + final int ret = (readTTFUByte() << 8) + readTTFUByte(); + return ret; + } + + /** + * Write a USHort at a given position. + * + * @param pos The absolute position to write to + * @param val The value to write + * @throws IOException If EOF is reached + */ + public final void writeTTFUShort(long pos, int val) throws IOException { + if ((pos + 2) > fsize) { + throw new java.io.EOFException("Reached EOF"); + } + final byte b1 = (byte) ((val >> 8) & 0xff); + final byte b2 = (byte) (val & 0xff); + final int fileIndex = (int) pos; + file[fileIndex] = b1; + file[fileIndex + 1] = b2; + } + + /** + * Read 2 bytes signed at position pos without changing current position. + * + * @param pos The absolute position to read from + * @return One signed short + * @throws IOException If EOF is reached + */ + public final short readTTFShort(long pos) throws IOException { + final long cp = getCurrentPos(); + seekSet(pos); + final short ret = readTTFShort(); + seekSet(cp); + return ret; + } + + /** + * Read 2 bytes unsigned at position pos without changing current position. + * + * @param pos The absolute position to read from + * @return One unsigned short + * @throws IOException If EOF is reached + */ + public final int readTTFUShort(long pos) throws IOException { + long cp = getCurrentPos(); + seekSet(pos); + int ret = readTTFUShort(); + seekSet(cp); + return ret; + } + + /** + * Read 4 bytes. + * + * @return One signed integer + * @throws IOException If EOF is reached + */ + public final int readTTFLong() throws IOException { + long ret = readTTFUByte(); // << 8; + ret = (ret << 8) + readTTFUByte(); + ret = (ret << 8) + readTTFUByte(); + ret = (ret << 8) + readTTFUByte(); + + return (int) ret; + } + + /** + * Read 4 bytes. + * + * @return One unsigned integer + * @throws IOException If EOF is reached + */ + public final long readTTFULong() throws IOException { + long ret = readTTFUByte(); + ret = (ret << 8) + readTTFUByte(); + ret = (ret << 8) + readTTFUByte(); + ret = (ret << 8) + readTTFUByte(); + + return ret; + } + + /** + * Read a NUL terminated ISO-8859-1 string. + * + * @return A String + * @throws IOException If EOF is reached + */ + public final String readTTFString() throws IOException { + int i = current; + while (file[i++] != 0) { + if (i >= fsize) { + throw new java.io.EOFException("Reached EOF, file size=" + + fsize); + } + } + + byte[] tmp = new byte[i - current - 1]; + System.arraycopy(file, current, tmp, 0, i - current - 1); + return new String(tmp, "ISO-8859-1"); + } + + /** + * Read an ISO-8859-1 string of len bytes. + * + * @param len The length of the string to read + * @return A String + * @throws IOException If EOF is reached + */ + public final String readTTFString(int len) throws IOException { + if ((len + current) > fsize) { + throw new java.io.EOFException("Reached EOF, file size=" + fsize); + } + + byte[] tmp = new byte[len]; + System.arraycopy(file, current, tmp, 0, len); + current += len; + final String encoding; + if ((tmp.length > 0) && (tmp[0] == 0)) { + encoding = "UTF-16BE"; + } else { + encoding = "ISO-8859-1"; + } + return new String(tmp, encoding); + } + + /** + * Read an ISO-8859-1 string of len bytes. + * + * @param len The length of the string to read + * @param encodingID the string encoding id (presently ignored; always uses UTF-16BE) + * @return A String + * @throws IOException If EOF is reached + */ + public final String readTTFString(int len, int encodingID) throws IOException { + if ((len + current) > fsize) { + throw new java.io.EOFException("Reached EOF, file size=" + fsize); + } + + byte[] tmp = new byte[len]; + System.arraycopy(file, current, tmp, 0, len); + current += len; + final String encoding; + encoding = "UTF-16BE"; //Use this for all known encoding IDs for now + return new String(tmp, encoding); + } + + /** + * Return a copy of the internal array + * + * @param offset The absolute offset to start reading from + * @param length The number of bytes to read + * @return An array of bytes + * @throws IOException if out of bounds + */ + public byte[] getBytes(int offset, + int length) throws IOException { + if ((offset + length) > fsize) { + throw new IOException("Reached EOF"); + } + + byte[] ret = new byte[length]; + System.arraycopy(file, offset, ret, 0, length); + return ret; + } + + /** + * Returns the full byte array representation of the file. + * + * @return byte array. + */ + public byte[] getAllBytes() { + return file; + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/GlyphTable.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/GlyphTable.java new file mode 100644 index 0000000..50d6c2a --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/GlyphTable.java @@ -0,0 +1,1442 @@ +/* + * Copyright (C) 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. + * + */ + +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.*; + +/** + *

Base class for all advanced typographic glyph tables.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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> lookups; + + // map from lookup identifiers to lookup tables + private Map lookupTables; + + // cache for lookups matching + private Map>> 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> 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 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 getLookupTables() { + TreeSet lids = new TreeSet<>(lookupTables.keySet()); + List 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 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 matchLookupSpecs(String script, String language, String feature) { + Set keys = lookups.keySet(); + List 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 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> matchLookups(String script, String language, String feature) { + LookupSpec lsm = new LookupSpec(script, language, feature, true, true); + Map> lm = matchedLookups.get(lsm); + if (lm == null) { + lm = new LinkedHashMap<>(); + List 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 findLookupTables(LookupSpec ls) { + TreeSet lts = new TreeSet<>(); + List 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> lookups) { + TreeSet uss = new TreeSet(); + for (String feature : features) { + for (Map.Entry> e : lookups.entrySet()) { + LookupSpec ls = e.getKey(); + if (ls.getFeature().equals(feature)) { + List 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 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 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 LookupTable 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 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 subtables) { + this.id = id; + this.idOrdinal = Integer.parseInt(id.substring(2)); + this.subtables = new LinkedList(); + 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 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 lookupTables) { + if (!frozen) { + GlyphSubtable[] sta = getSubtables(); + resolveLookupReferences(sta, lookupTables); + this.subtablesArray = sta; + this.frozen = true; + } + } + + private void resolveLookupReferences(GlyphSubtable[] subtables, Map 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 makeSingleton(GlyphSubtable subtable) { + if (subtable == null) { + return null; + } else { + List stl = new ArrayList(1); + stl.add(subtable); + return stl; + } + } + + } + + /** + * The UseSpec 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 RuleLookup 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 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 Rule 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 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 GlyphSequenceRule class implements a subclass of Rule + * 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 ClassSequenceRule class implements a subclass of Rule + * 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 CoverageSequenceRule class implements a subclass of Rule + * 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 ChainedGlyphSequenceRule class implements a subclass of GlyphSequenceRule + * 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 ChainedClassSequenceRule class implements a subclass of ClassSequenceRule + * 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 ChainedCoverageSequenceRule class implements a subclass of CoverageSequenceRule + * 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 RuleSet 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 lookupTables) { + if (rules != null) { + for (Rule r : rules) { + if (r != null) { + r.resolveLookupReferences(lookupTables); + } + } + } + } + + /** + * {@inheritDoc} + */ + public String toString() { + return "{ rules = " + Arrays.toString(rules) + " }"; + } + + } + + /** + * The HomogenousRuleSet class implements a collection of rules, which + * must be the same rule type (i.e., same concrete rule class) or null. + */ + public static class HomogeneousRuleSet extends RuleSet { + + /** + * Instantiate a Homogeneous Rule Set. + * + * @param rules the rules + * @throws AdvancedTypographicTableFormatException if some rule[i] is not an instance of rule[0] + */ + public HomogeneousRuleSet(Rule[] rules) throws AdvancedTypographicTableFormatException { + super(rules); + // find first non-null rule + Rule r0 = null; + for (int i = 1, n = rules.length; (r0 == null) && (i < n); i++) { + if (rules[i] != null) { + r0 = rules[i]; + } + } + // enforce rule instance homogeneity + if (r0 != null) { + Class c = r0.getClass(); + for (int i = 1, n = rules.length; i < n; i++) { + Rule r = rules[i]; + if ((r != null) && !c.isInstance(r)) { + throw new AdvancedTypographicTableFormatException("rules[" + i + "] is not an instance of " + c.getName()); + } + } + } + + } + + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFDirTabEntry.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFDirTabEntry.java new file mode 100644 index 0000000..cf1ad39 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFDirTabEntry.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.truetype; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +/** + * This class represents an entry to a TrueType font's Dir Tab. + */ +public class OFDirTabEntry { + + private byte[] tag = new byte[4]; + private long checksum; + private long offset; + private long length; + + public OFDirTabEntry() { + } + + public OFDirTabEntry(long offset, long length) { + this.offset = offset; + this.length = length; + } + + /** + * Read Dir Tab. + * + * @param in font file reader + * @return tag name + * @throws IOException upon I/O exception + */ + public String read(FontFileReader in) throws IOException { + tag[0] = in.readTTFByte(); + tag[1] = in.readTTFByte(); + tag[2] = in.readTTFByte(); + tag[3] = in.readTTFByte(); + + checksum = in.readTTFLong(); + offset = in.readTTFULong(); + length = in.readTTFULong(); + + return getTagString(); + } + + @Override + public String toString() { + return "Read dir tab [" + Arrays.toString(tag) + "]" + + " offset: " + offset + + " length: " + length + + " name: " + getTagString(); + } + + /** + * Returns the checksum. + * + * @return int + */ + public long getChecksum() { + return checksum; + } + + /** + * Returns the length. + * + * @return long + */ + public long getLength() { + return length; + } + + /** + * Returns the offset. + * + * @return long + */ + public long getOffset() { + return offset; + } + + /** + * Returns the tag bytes. + * + * @return byte[] + */ + public byte[] getTag() { + return tag; + } + + /** + * Returns the tag bytes. + * + * @return byte[] + */ + public String getTagString() { + try { + return new String(tag, "ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + return this.toString(); // Should never happen. + } + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFMtxEntry.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFMtxEntry.java new file mode 100644 index 0000000..0fc77b1 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFMtxEntry.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.truetype; + +import java.util.List; + +/** + * This class represents a TrueType Mtx Entry. + */ +public class OFMtxEntry { + + private int wx; + private int lsb; + private String name = ""; + private int index; + private List unicodeIndex = new java.util.ArrayList(); + private int[] boundingBox = new int[4]; + private long offset; + private byte found; + + /** + * Returns a String representation of this object. + * + * @param t TTFFile to use for unit conversion + * @return String String representation + */ + public String toString(TTFFile t) { + return "Glyph " + name + " index: " + getIndexAsString() + " bbox [" + + t.convertTTFUnit2PDFUnit(boundingBox[0]) + " " + + t.convertTTFUnit2PDFUnit(boundingBox[1]) + " " + + t.convertTTFUnit2PDFUnit(boundingBox[2]) + " " + + t.convertTTFUnit2PDFUnit(boundingBox[3]) + "] wx: " + + t.convertTTFUnit2PDFUnit(wx); + } + + /** + * Returns the boundingBox. + * + * @return int[] + */ + public int[] getBoundingBox() { + return boundingBox; + } + + /** + * Sets the boundingBox. + * + * @param boundingBox The boundingBox to set + */ + public void setBoundingBox(int[] boundingBox) { + this.boundingBox = boundingBox; + } + + /** + * Returns the found. + * + * @return byte + */ + public byte getFound() { + return found; + } + + /** + * Returns the index. + * + * @return int + */ + public int getIndex() { + return index; + } + + /** + * Determines whether this index represents a reserved character. + * + * @return True if it is reserved + */ + public boolean isIndexReserved() { + return (getIndex() >= 32768) && (getIndex() <= 65535); + } + + /** + * Returns a String representation of the index taking into account if + * the index is in the reserved range. + * + * @return index as String + */ + public String getIndexAsString() { + if (isIndexReserved()) { + return Integer.toString(getIndex()) + " (reserved)"; + } else { + return Integer.toString(getIndex()); + } + } + + /** + * Returns the lsb. + * + * @return int + */ + public int getLsb() { + return lsb; + } + + /** + * Returns the name. + * + * @return String + */ + public String getName() { + return name; + } + + /** + * Returns the offset. + * + * @return long + */ + public long getOffset() { + return offset; + } + + /** + * Returns the unicodeIndex. + * + * @return List + */ + public List getUnicodeIndex() { + return unicodeIndex; + } + + /** + * Returns the wx. + * + * @return int + */ + public int getWx() { + return wx; + } + + /** + * Sets the found. + * + * @param found The found to set + */ + public void setFound(byte found) { + this.found = found; + } + + /** + * Sets the index. + * + * @param index The index to set + */ + public void setIndex(int index) { + this.index = index; + } + + /** + * Sets the lsb. + * + * @param lsb The lsb to set + */ + public void setLsb(int lsb) { + this.lsb = lsb; + } + + /** + * Sets the name. + * + * @param name The name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * Sets the offset. + * + * @param offset The offset to set + */ + public void setOffset(long offset) { + this.offset = offset; + } + + /** + * Sets the wx. + * + * @param wx The wx to set + */ + public void setWx(int wx) { + this.wx = wx; + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFTableName.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFTableName.java new file mode 100644 index 0000000..2172f0e --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OFTableName.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.truetype; + +/** + * Represents table names as found in a TrueType font's Table Directory. + * TrueType fonts may have custom tables so we cannot use an enum. + */ +public final class OFTableName { + + /** + * The first table in a TrueType font file containing metadata about other tables. + */ + public static final OFTableName TABLE_DIRECTORY = new OFTableName("tableDirectory"); + + /** + * Baseline data + */ + public static final OFTableName BASE = new OFTableName("BASE"); + + /** + * CFF data/ + */ + public static final OFTableName CFF = new OFTableName("CFF "); + + /** + * Embedded bitmap data. + */ + public static final OFTableName EBDT = new OFTableName("EBDT"); + + /** + * Embedded bitmap location data. + */ + public static final OFTableName EBLC = new OFTableName("EBLC"); + + /** + * Embedded bitmap scaling data. + */ + public static final OFTableName EBSC = new OFTableName("EBSC"); + + /** + * A FontForge specific table. + */ + public static final OFTableName FFTM = new OFTableName("FFTM"); + + /** + * Divides glyphs into various classes that make using the GPOS/GSUB tables easier. + */ + public static final OFTableName GDEF = new OFTableName("GDEF"); + + /** + * Provides kerning information, mark-to-base, etc. for opentype fonts. + */ + public static final OFTableName GPOS = new OFTableName("GPOS"); + + /** + * Provides ligature information, swash, etc. for opentype fonts. + */ + public static final OFTableName GSUB = new OFTableName("GSUB"); + + /** + * Linear threshold table. + */ + public static final OFTableName LTSH = new OFTableName("LTSH"); + + /** + * OS/2 and Windows specific metrics. + */ + public static final OFTableName OS2 = new OFTableName("OS/2"); + + /** + * PCL 5 data. + */ + public static final OFTableName PCLT = new OFTableName("PCLT"); + + /** + * Vertical Device Metrics table. + */ + public static final OFTableName VDMX = new OFTableName("VDMX"); + + /** + * Character to glyph mapping. + */ + public static final OFTableName CMAP = new OFTableName("cmap"); + + /** + * Control Value Table. + */ + public static final OFTableName CVT = new OFTableName("cvt "); + + /** + * Font program. + */ + public static final OFTableName FPGM = new OFTableName("fpgm"); + + /** + * Grid-fitting and scan conversion procedure (grayscale). + */ + public static final OFTableName GASP = new OFTableName("gasp"); + + /** + * Glyph data. + */ + public static final OFTableName GLYF = new OFTableName("glyf"); + + /** + * Horizontal device metrics. + */ + public static final OFTableName HDMX = new OFTableName("hdmx"); + + /** + * Font header. + */ + public static final OFTableName HEAD = new OFTableName("head"); + + /** + * Horizontal header. + */ + public static final OFTableName HHEA = new OFTableName("hhea"); + + /** + * Horizontal metrics. + */ + public static final OFTableName HMTX = new OFTableName("hmtx"); + + /** + * Kerning. + */ + public static final OFTableName KERN = new OFTableName("kern"); + + /** + * Index to location. + */ + public static final OFTableName LOCA = new OFTableName("loca"); + + /** + * Maximum profile. + */ + public static final OFTableName MAXP = new OFTableName("maxp"); + + /** + * Naming table. + */ + public static final OFTableName NAME = new OFTableName("name"); + + /** + * PostScript information. + */ + public static final OFTableName POST = new OFTableName("post"); + + /** + * CVT Program. + */ + public static final OFTableName PREP = new OFTableName("prep"); + + /** + * Vertical Metrics header. + */ + public static final OFTableName VHEA = new OFTableName("vhea"); + + /** + * Vertical Metrics. + */ + public static final OFTableName VMTX = new OFTableName("vmtx"); + + private final String name; + + private OFTableName(String name) { + this.name = name; + } + + /** + * Returns the name of the table as it should be in the Directory Table. + */ + public String getName() { + return name; + } + + /** + * Returns an instance of this class corresponding to the given string representation. + * + * @param tableName table name as in the Table Directory + * @return TTFTableName + */ + public static OFTableName getValue(String tableName) { + if (tableName != null) { + return new OFTableName(tableName); + } + throw new IllegalArgumentException("A TrueType font table name must not be null"); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof OFTableName)) { + return false; + } + OFTableName to = (OFTableName) o; + return this.name.equals(to.getName()); + } + + @Override + public String toString() { + return name; + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OpenFont.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OpenFont.java new file mode 100644 index 0000000..c37a012 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/OpenFont.java @@ -0,0 +1,1734 @@ +/* + * Copyright (C) 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. + * + */ + +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 dirTabs; + + private Map> rawKerningTab; // for CIDs + private Map> kerningTab; // for CIDs + private Map> ansiKerningTab; // For winAnsiEncoding + private List cmaps; + protected List 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 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> ansiIndex; + + // internal mapping of glyph indexes to unicode indexes + // used for quick mappings in this class + private final Map glyphToUnicodeMap = new HashMap<>(); + private final Map unicodeToGlyphMap = new HashMap<>(); + + private boolean isCFF; + + // advanced typographic table support + protected boolean useAdvanced; + protected OTFAdvancedTypographicTableReader advancedTableReader; + + /** + * Version of the PostScript table (post) 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 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 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 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 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 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 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> getRawKerning() { + return rawKerningTab; + } + + /** + * Returns the kerning table. + * + * @return Map The kerning table + */ + public Map> getKerning() { + return kerningTab; + } + + /** + * Returns the ANSI kerning table. + * + * @return Map The ANSI kerning table + */ + public Map> 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 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 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> e1 : kerningTab.entrySet()) { + Integer unicodeKey1 = e1.getKey(); + Integer cidKey1 = unicodeToGlyph(unicodeKey1); + Map akpx = new HashMap<>(); + Map ckpx = e1.getValue(); + + for (Entry 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> sortedDirTabs = sortDirTabMap(dirTabs); + byte[] file = fontFile.getAllBytes(); + TTFTableOutputStream tableOut = ttfOut.getTableOutputStream(); + TTFGlyphOutputStream glyphOut = ttfOut.getGlyphOutputStream(); + ttfOut.startFontStream(); + for (Entry 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> + sortDirTabMap(Map directoryTabs) { + SortedSet> sortedSet + = new TreeSet<>( + new Comparator<>() { + + public int compare(Entry o1, + Entry 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 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 getTTCnames(FontFileReader in) throws IOException { + this.fontFile = in; + + List 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 ret = new ArrayList<>(); + for (int i = 32; i < Glyphs.WINANSI_ENCODING.length; i++) { + if (unicode == Glyphs.WINANSI_ENCODING[i]) { + ret.add(Integer.valueOf(i)); + } + } + return ret.toArray(new Integer[ret.size()]); + } + + private String formatUnitsForDebug(int units) { + return units + " -> " + convertTTFUnit2PDFUnit(units) + " internal units"; + } + + /** + * Map a glyph index to the corresponding unicode code point + * + * @param glyphIndex + * @return unicode code point + */ + public Integer glyphToUnicode(int glyphIndex) { + return glyphToUnicodeMap.get(Integer.valueOf(glyphIndex)); + } + + /** + * Map a unicode code point to the corresponding glyph index + * + * @param unicodeIndex unicode code point + * @return glyph index + */ + public Integer unicodeToGlyph(int unicodeIndex) throws IOException { + final Integer result + = unicodeToGlyphMap.get(Integer.valueOf(unicodeIndex)); + if (result == null) { + throw new IOException( + "Glyph index not found for unicode value " + unicodeIndex); + } + return result; + } + + String getGlyphName(int glyphIndex) { + return mtxTab[glyphIndex].getName(); + } + + /** + * Determine if advanced (typographic) table is present. + * + * @return true if advanced (typographic) table is present + */ + public boolean hasAdvancedTable() { + if (advancedTableReader != null) { + return advancedTableReader.hasAdvancedTable(); + } else { + return false; + } + } + + /** + * Returns the GDEF table or null if none present. + * + * @return the GDEF table + */ + public GlyphDefinitionTable getGDEF() { + if (advancedTableReader != null) { + return advancedTableReader.getGDEF(); + } else { + return null; + } + } + + /** + * Returns the GSUB table or null if none present. + * + * @return the GSUB table + */ + public GlyphSubstitutionTable getGSUB() { + if (advancedTableReader != null) { + return advancedTableReader.getGSUB(); + } else { + return null; + } + } + + /** + * Returns the GPOS table or null if none present. + * + * @return the GPOS table + */ + public GlyphPositioningTable getGPOS() { + if (advancedTableReader != null) { + return advancedTableReader.getGPOS(); + } else { + return null; + } + } + + public String getCopyrightNotice() { + return notice; + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFFile.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFFile.java new file mode 100644 index 0000000..06c0a9d --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFFile.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.truetype; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Reads a TrueType file or a TrueType Collection. + * The TrueType spec can be found at the Microsoft. + * Typography site: http://www.microsoft.com/truetype/ + */ +public class TTFFile extends OpenFont { + + /** + * Reads a TTF file + * + * @param file The font file + * @return The TrueType file + * @throws IOException if an IO error occurs + */ + public static TTFFile open(File file) throws IOException { + return open(new FileInputStream(file)); + } + + /** + * Reads a TTF file from an InputStream + * + * @param is InputStream to read from + * @return The TrueType file + * @throws IOException if an IO error occurs + */ + public static TTFFile open(InputStream is) throws IOException { + TTFFile ttfFile = new TTFFile(); + ttfFile.readFont(new FontFileReader(is)); + return ttfFile; + } + + public TTFFile() { + this(true, false); + } + + /** + * Constructor + * + * @param useKerning true if kerning data should be loaded + * @param useAdvanced true if advanced typographic tables should be loaded + */ + public TTFFile(boolean useKerning, boolean useAdvanced) { + super(useKerning, useAdvanced); + } + + /** + * Read the "name" table. + * + * @throws IOException In case of a I/O problem + */ + protected void readName() throws IOException { + seekTab(fontFile, OFTableName.NAME, 2); + int i = fontFile.getCurrentPos(); + int n = fontFile.readTTFUShort(); + int j = fontFile.readTTFUShort() + i - 2; + i += 2 * 2; + + while (n-- > 0) { + fontFile.seekSet(i); + final int platformID = fontFile.readTTFUShort(); + final int encodingID = fontFile.readTTFUShort(); + final int languageID = fontFile.readTTFUShort(); + + int k = fontFile.readTTFUShort(); + int l = fontFile.readTTFUShort(); + + if (((platformID == 1 || platformID == 3) + && (encodingID == 0 || encodingID == 1))) { + fontFile.seekSet(j + fontFile.readTTFUShort()); + String txt; + if (platformID == 3) { + txt = fontFile.readTTFString(l, encodingID); + } else { + txt = fontFile.readTTFString(l); + } + + switch (k) { + case 0: + if (notice.length() == 0) { + notice = txt; + } + break; + case 1: //Font Family Name + case 16: //Preferred Family + familyNames.add(txt); + break; + case 2: + if (subFamilyName.length() == 0) { + subFamilyName = txt; + } + break; + case 4: + if (fullName.length() == 0 || (platformID == 3 && languageID == 1033)) { + fullName = txt; + } + break; + case 6: + if (postScriptName.length() == 0) { + postScriptName = txt; + } + break; + default: + break; + } + } + i += 6 * 2; + } + } + + /** + * Read the "glyf" table to find the bounding boxes. + * + * @throws IOException In case of a I/O problem + */ + private void readGlyf() throws IOException { + OFDirTabEntry dirTab = dirTabs.get(OFTableName.GLYF); + if (dirTab == null) { + throw new IOException("glyf table not found, cannot continue"); + } + for (int i = 0; i < (numberOfGlyphs - 1); i++) { + if (mtxTab[i].getOffset() != mtxTab[i + 1].getOffset()) { + fontFile.seekSet(dirTab.getOffset() + mtxTab[i].getOffset()); + fontFile.skip(2); + final int[] bbox = { + fontFile.readTTFShort(), + fontFile.readTTFShort(), + fontFile.readTTFShort(), + fontFile.readTTFShort()}; + mtxTab[i].setBoundingBox(bbox); + } else { + mtxTab[i].setBoundingBox(mtxTab[0].getBoundingBox()); + } + } + + long n = (dirTabs.get(OFTableName.GLYF)).getOffset(); + for (int i = 0; i < numberOfGlyphs; i++) { + if ((i + 1) >= mtxTab.length + || mtxTab[i].getOffset() != mtxTab[i + 1].getOffset()) { + fontFile.seekSet(n + mtxTab[i].getOffset()); + fontFile.skip(2); + final int[] bbox = { + fontFile.readTTFShort(), + fontFile.readTTFShort(), + fontFile.readTTFShort(), + fontFile.readTTFShort()}; + mtxTab[i].setBoundingBox(bbox); + } else { + final int bbox0 = mtxTab[0].getBoundingBox()[0]; + final int[] bbox = {bbox0, bbox0, bbox0, bbox0}; + mtxTab[i].setBoundingBox(bbox); + } + } + } + + @Override + protected void updateBBoxAndOffset() throws IOException { + readIndexToLocation(); + readGlyf(); + } + + /** + * Read the "loca" table. + * + * @throws IOException In case of a I/O problem + */ + protected final void readIndexToLocation() + throws IOException { + if (!seekTab(fontFile, OFTableName.LOCA, 0)) { + throw new IOException("'loca' table not found, happens when the font file doesn't" + + " contain TrueType outlines (trying to read an OpenType CFF font maybe?)"); + } + for (int i = 0; i < numberOfGlyphs; i++) { + mtxTab[i].setOffset(locaFormat == 1 ? fontFile.readTTFULong() + : (fontFile.readTTFUShort() << 1)); + } + lastLoca = (locaFormat == 1 ? fontFile.readTTFULong() + : (fontFile.readTTFUShort() << 1)); + } + + /** + * Gets the last location of the glyf table + * + * @return The last location as a long + */ + public long getLastGlyfLocation() { + return lastLoca; + } + + @Override + protected void initializeFont(FontFileReader in) throws IOException { + fontFile = in; + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFGlyphOutputStream.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFGlyphOutputStream.java new file mode 100644 index 0000000..0b38935 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFGlyphOutputStream.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.truetype; + +import java.io.IOException; + +/** + * An interface for writing individual glyphs from the glyf table of a TrueType font to an output stream. + */ +public interface TTFGlyphOutputStream { + + /** + * Begins the streaming of glyphs. + */ + void startGlyphStream() throws IOException; + + /** + * Streams an individual glyph from the given byte array. + * + * @param glyphData the source of the glyph data to stream from + * @param offset the position in the glyph data where the glyph starts + * @param size the size of the glyph data in bytes + */ + void streamGlyph(byte[] glyphData, int offset, int size) throws IOException; + + /** + * Ends the streaming of glyphs. + */ + void endGlyphStream() throws IOException; + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFOutputStream.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFOutputStream.java new file mode 100644 index 0000000..8b3d334 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFOutputStream.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.truetype; + +import java.io.IOException; + +/** + * An interface for writing a TrueType font to an output stream. + */ +public interface TTFOutputStream { + + /** + * Starts writing the font. + */ + void startFontStream() throws IOException; + + /** + * Returns an object for streaming TrueType tables. + */ + TTFTableOutputStream getTableOutputStream(); + + /** + * Returns an object for streaming TrueType glyphs in the glyf table. + */ + TTFGlyphOutputStream getGlyphOutputStream(); + + /** + * Ends writing the font. + */ + void endFontStream() throws IOException; + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFTableOutputStream.java b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFTableOutputStream.java new file mode 100644 index 0000000..edfa9a6 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/truetype/TTFTableOutputStream.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.truetype; + +import java.io.IOException; + +/** + * An interface for writing a TrueType table to an output stream. + */ +public interface TTFTableOutputStream { + + /** + * Streams a table from the given byte array. + * + * @param ttfData the source of the table to stream from + * @param offset the position in the byte array where the table starts + * @param size the size of the table in bytes + */ + void streamTable(byte[] ttfData, int offset, int size) throws IOException; +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/util/CharUtilities.java b/fontparser/src/main/java/com/jaredrummler/fontreader/util/CharUtilities.java new file mode 100644 index 0000000..f5b9fe7 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/util/CharUtilities.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 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. + * + */ + +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; + } + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphSequence.java b/fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphSequence.java new file mode 100644 index 0000000..e85cc09 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphSequence.java @@ -0,0 +1,655 @@ +/* + * Copyright (C) 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. + * + */ + +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 + +/** + *

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.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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 copy 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 count 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 copy 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 at glyph sequence OFFSET. + * + * @param offset offset (index) into glyph sequence + * @param key predication key + * @param value predication value + */ + public void setPredication(int offset, String key, Object value) { + if (predications) { + CharAssociation[] aa = getAssociations(offset, 1); + CharAssociation ca = aa[0]; + ca.setPredication(key, value); + } + } + + /** + * Get predication KEY at glyph sequence OFFSET. + * + * @param offset offset (index) into glyph sequence + * @param key predication key + * @return predication KEY at OFFSET or null if none exists + */ + public Object getPredication(int offset, String key) { + if (predications) { + CharAssociation[] aa = getAssociations(offset, 1); + CharAssociation ca = aa[0]; + return ca.getPredication(key); + } else { + return null; + } + } + + /** + * Compare glyphs. + * + * @param gb buffer containing glyph indices with which this glyph sequence's glyphs are to be compared + * @return zero if glyphs are the same, otherwise returns 1 or -1 according to whether this glyph sequence's + * glyphs are lexicographically greater or lesser than the glyphs in the specified string buffer + */ + public int compareGlyphs(IntBuffer gb) { + int ng = getGlyphCount(); + for (int i = 0, n = gb.limit(); i < n; i++) { + if (i < ng) { + int g1 = glyphs.get(i); + int g2 = gb.get(i); + if (g1 > g2) { + return 1; + } else if (g1 < g2) { + return -1; + } + } else { + return -1; // this gb is a proper prefix of specified gb + } + } + return 0; // same lengths with no difference + } + + /** + * {@inheritDoc} + */ + public Object clone() { + try { + GlyphSequence gs = (GlyphSequence) super.clone(); + gs.characters = copyBuffer(characters); + gs.glyphs = copyBuffer(glyphs); + gs.associations = copyAssociations(associations); + return gs; + } catch (CloneNotSupportedException e) { + return null; + } + } + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append('{'); + sb.append("chars = ["); + sb.append(characters); + sb.append("], glyphs = ["); + sb.append(glyphs); + sb.append("], associations = ["); + sb.append(associations); + sb.append("]"); + sb.append('}'); + return sb.toString(); + } + + /** + * Determine if two arrays of glyphs are identical. + * + * @param ga1 first glyph array + * @param ga2 second glyph array + * @return true if arrays are botth null or both non-null and have identical elements + */ + public static boolean sameGlyphs(int[] ga1, int[] ga2) { + if (ga1 == ga2) { + return true; + } else if ((ga1 == null) || (ga2 == null)) { + return false; + } else if (ga1.length != ga2.length) { + return false; + } else { + for (int i = 0, n = ga1.length; i < n; i++) { + if (ga1[i] != ga2[i]) { + return false; + } + } + return true; + } + } + + /** + * Concatenante glyph arrays. + * + * @param bga backtrack glyph array + * @param iga input glyph array + * @param lga lookahead glyph array + * @return new integer buffer containing concatenated glyphs + */ + public static IntBuffer concatGlyphs(int[] bga, int[] iga, int[] lga) { + int ng = 0; + if (bga != null) { + ng += bga.length; + } + if (iga != null) { + ng += iga.length; + } + if (lga != null) { + ng += lga.length; + } + IntBuffer gb = IntBuffer.allocate(ng); + if (bga != null) { + gb.put(bga); + } + if (iga != null) { + gb.put(iga); + } + if (lga != null) { + gb.put(lga); + } + gb.flip(); + return gb; + } + + /** + * Concatenante association arrays. + * + * @param baa backtrack association array + * @param iaa input association array + * @param laa lookahead association array + * @return new list containing concatenated associations + */ + public static List concatAssociations(CharAssociation[] baa, CharAssociation[] iaa, CharAssociation[] laa) { + int na = 0; + if (baa != null) { + na += baa.length; + } + if (iaa != null) { + na += iaa.length; + } + if (laa != null) { + na += laa.length; + } + if (na > 0) { + List gl = new ArrayList(na); + if (baa != null) { + for (int i = 0; i < baa.length; i++) { + gl.add(baa[i]); + } + } + if (iaa != null) { + for (int i = 0; i < iaa.length; i++) { + gl.add(iaa[i]); + } + } + if (laa != null) { + for (int i = 0; i < laa.length; i++) { + gl.add(laa[i]); + } + } + return gl; + } else { + return null; + } + } + + /** + * Join (concatenate) glyph sequences. + * + * @param gs original glyph sequence from which to reuse character array reference + * @param sa array of glyph sequences, whose glyph arrays and association lists are to be concatenated + * @return new glyph sequence referring to character array of GS and concatenated glyphs and associations of SA + */ + public static GlyphSequence join(GlyphSequence gs, GlyphSequence[] sa) { + assert sa != null; + int tg = 0; + int ta = 0; + for (int i = 0, n = sa.length; i < n; i++) { + GlyphSequence s = sa[i]; + IntBuffer ga = s.getGlyphs(); + assert ga != null; + int ng = ga.limit(); + List al = s.getAssociations(); + assert al != null; + int na = al.size(); + assert na == ng; + tg += ng; + ta += na; + } + IntBuffer uga = IntBuffer.allocate(tg); + ArrayList ual = new ArrayList(ta); + for (int i = 0, n = sa.length; i < n; i++) { + GlyphSequence s = sa[i]; + uga.put(s.getGlyphs()); + ual.addAll(s.getAssociations()); + } + return new GlyphSequence(gs.getCharacters(), uga, ual, gs.getPredications()); + } + + /** + * Reorder sequence such that [SOURCE,SOURCE+COUNT) is moved just prior to TARGET. + * + * @param gs input sequence + * @param source index of sub-sequence to reorder + * @param count length of sub-sequence to reorder + * @param target index to which source sub-sequence is to be moved + * @return reordered sequence (or original if no reordering performed) + */ + public static GlyphSequence reorder(GlyphSequence gs, int source, int count, int target) { + if (source != target) { + int ng = gs.getGlyphCount(); + int[] ga = gs.getGlyphArray(false); + int[] nga = new int[ng]; + CharAssociation[] aa = gs.getAssociations(0, ng); + CharAssociation[] naa = new CharAssociation[ng]; + if (source < target) { + int t = 0; + for (int s = 0, e = source; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = source + count, e = target; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = source, e = source + count; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = target, e = ng; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + } else { + int t = 0; + for (int s = 0, e = target; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = source, e = source + count; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = target, e = source; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = source + count, e = ng; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + } + return new GlyphSequence(gs, null, nga, null, null, naa, null); + } else { + return gs; + } + } + + private static int[] toArray(IntBuffer ib) { + if (ib != null) { + int n = ib.limit(); + int[] ia = new int[n]; + ib.get(ia, 0, n); + return ia; + } else { + return new int[0]; + } + } + + private static List makeIdentityAssociations(int numChars, int numGlyphs) { + int nc = numChars; + int ng = numGlyphs; + List av = new ArrayList(ng); + for (int i = 0, n = ng; i < n; i++) { + int k = (i > nc) ? nc : i; + av.add(new CharAssociation(i, (k == nc) ? 0 : 1)); + } + return av; + } + + private static IntBuffer copyBuffer(IntBuffer ib) { + if (ib != null) { + int[] ia = new int[ib.capacity()]; + int p = ib.position(); + int l = ib.limit(); + System.arraycopy(ib.array(), 0, ia, 0, ia.length); + return IntBuffer.wrap(ia, p, l - p); + } else { + return null; + } + } + + private static List copyAssociations(List ca) { + if (ca != null) { + return new ArrayList(ca); + } else { + return ca; + } + } + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphTester.java b/fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphTester.java new file mode 100644 index 0000000..efb9e2e --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/util/GlyphTester.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.util; + +/** + *

Interface for testing glyph properties according to glyph identifier.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface GlyphTester { + + /** + * Perform a test on a glyph identifier. + * + * @param gi glyph identififer + * @param flags that apply to lookup in scope + * @return true if test is satisfied + */ + boolean test(int gi, int flags); + +} diff --git a/fontparser/src/main/java/com/jaredrummler/fontreader/util/ScriptContextTester.java b/fontparser/src/main/java/com/jaredrummler/fontreader/util/ScriptContextTester.java new file mode 100644 index 0000000..a7da671 --- /dev/null +++ b/fontparser/src/main/java/com/jaredrummler/fontreader/util/ScriptContextTester.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 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. + * + */ + +package com.jaredrummler.fontreader.util; + +import com.jaredrummler.fontreader.complexscripts.fonts.GlyphContextTester; + +/** + *

Interface for providing script specific context testers.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +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); + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b7bc9a1..a67fe34 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,4 +2,5 @@ rootProject.name = "factbooks" include("externals") +include("fontparser") //include("fightgame") diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/fonts.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/fonts.kt index 701ccb1..46f72bc 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/fonts.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/fonts.kt @@ -1,21 +1,25 @@ package info.mechyrdia.lore +import com.jaredrummler.fontreader.truetype.FontFileReader +import com.jaredrummler.fontreader.truetype.TTFFile +import com.jaredrummler.fontreader.util.GlyphSequence import info.mechyrdia.Configuration import info.mechyrdia.yieldThread import io.ktor.util.* import java.awt.Font -import java.awt.RenderingHints import java.awt.Shape import java.awt.geom.AffineTransform import java.awt.geom.GeneralPath import java.awt.geom.PathIterator import java.awt.image.BufferedImage +import java.io.ByteArrayInputStream import java.io.File +import java.io.IOException +import java.nio.IntBuffer import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty -import kotlin.text.toCharArray object MechyrdiaSansFont { enum class Alignment(val amount: Double) { @@ -23,39 +27,49 @@ object MechyrdiaSansFont { } fun renderTextToSvg(text: String, bold: Boolean, italic: Boolean, align: Alignment): String { - return layoutText(text, getFont(bold, italic), align.amount).toSvgDocument() + val (file, font) = getFont(bold, italic) + return layoutText(text, file, font, align.amount).toSvgDocument(96.0 / file.unitsPerEm) } - private const val DEFAULT_FONT_SIZE = 48f - private val fontsRoot = File(Configuration.CurrentConfiguration.rootDir, "fonts") - private fun loadedFont(fontName: String): ReadOnlyProperty { - return object : ReadOnlyProperty { + private fun loadedFont(fontName: String): ReadOnlyProperty> { + return object : ReadOnlyProperty> { + 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 { + 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 { return getValueLock.withLock { + val file = loadedFile val font = loadedFont val lastMod = fontFile.lastModified() - if (font == null || lastLoaded < lastMod) - loadFont().also { - loadedFont = it + if (file == null || font == null || lastLoaded < lastMod) + loadFont().also { (file, font) -> + loadedFile = file + loadedFont = font lastLoaded = lastMod } - else font + else file to font } } } @@ -67,56 +81,147 @@ object MechyrdiaSansFont { private val mechyrdiaSansBI by loadedFont("mechyrdia-sans-bold-italic") private val mechyrdiaSansFonts = listOf(::mechyrdiaSans, ::mechyrdiaSansI, ::mechyrdiaSansB, ::mechyrdiaSansBI) - private fun getFont(bold: Boolean, italic: Boolean): Font { + private fun getFont(bold: Boolean, italic: Boolean): Pair { 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 { + 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): 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("") val viewBox = bounds2D - appendLine("") + appendLine("") appendLine(toSvgPath()) appendLine("") }