| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2016 The Qt Company Ltd. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the Qt Virtual Keyboard module of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 or (at your option) any later version |
| 20 | ** approved by the KDE Free Qt Foundation. The licenses are as published by |
| 21 | ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
| 22 | ** included in the packaging of this file. Please review the following |
| 23 | ** information to ensure the GNU General Public License requirements will |
| 24 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 25 | ** |
| 26 | ** $QT_END_LICENSE$ |
| 27 | ** |
| 28 | ****************************************************************************/ |
| 29 | |
| 30 | #include "hangul_p.h" |
| 31 | |
| 32 | QT_BEGIN_NAMESPACE |
| 33 | namespace QtVirtualKeyboard { |
| 34 | |
| 35 | const QList<ushort> Hangul::initials = QList<ushort>() |
| 36 | << 0x3131 << 0x3132 << 0x3134 << 0x3137 << 0x3138 << 0x3139 << 0x3141 |
| 37 | << 0x3142 << 0x3143 << 0x3145 << 0x3146 << 0x3147 << 0x3148 << 0x3149 |
| 38 | << 0x314A << 0x314B << 0x314C << 0x314D << 0x314E; |
| 39 | const QList<ushort> Hangul::finals = QList<ushort>() |
| 40 | << 0x0000 << 0x3131 << 0x3132 << 0x3133 << 0x3134 << 0x3135 << 0x3136 |
| 41 | << 0x3137 << 0x3139 << 0x313A << 0x313B << 0x313C << 0x313D << 0x313E |
| 42 | << 0x313F << 0x3140 << 0x3141 << 0x3142 << 0x3144 << 0x3145 << 0x3146 |
| 43 | << 0x3147 << 0x3148 << 0x314A << 0x314B << 0x314C << 0x314D << 0x314E; |
| 44 | const QMap<ushort, Hangul::HangulMedialIndex> Hangul::doubleMedialMap = |
| 45 | Hangul::initDoubleMedialMap(); |
| 46 | const QMap<ushort, Hangul::HangulFinalIndex> Hangul::doubleFinalMap = |
| 47 | Hangul::initDoubleFinalMap(); |
| 48 | const int Hangul::SBase = 0xAC00; |
| 49 | const int Hangul::LBase = 0x1100; |
| 50 | const int Hangul::VBase = 0x314F; |
| 51 | const int Hangul::TBase = 0x11A7; |
| 52 | const int Hangul::LCount = 19; |
| 53 | const int Hangul::VCount = 21; |
| 54 | const int Hangul::TCount = 28; |
| 55 | const int Hangul::NCount = Hangul::VCount * Hangul::TCount; // 588 |
| 56 | const int Hangul::SCount = Hangul::LCount * Hangul::NCount; // 11172 |
| 57 | |
| 58 | /*! |
| 59 | \class QtVirtualKeyboard::Hangul |
| 60 | \internal |
| 61 | */ |
| 62 | |
| 63 | QString Hangul::decompose(const QString &source) |
| 64 | { |
| 65 | QString result; |
| 66 | const int len = source.length(); |
| 67 | for (int i = 0; i < len; i++) { |
| 68 | QChar ch = source.at(i); |
| 69 | int SIndex = (int)ch.unicode() - SBase; |
| 70 | if (SIndex >= 0 && SIndex < SCount) { |
| 71 | |
| 72 | // Decompose initial consonant |
| 73 | result.append(c: QChar((int)initials[SIndex / NCount])); |
| 74 | |
| 75 | // Decompose medial vowel and check if it consists of double Jamo |
| 76 | int VIndex = (SIndex % NCount) / TCount; |
| 77 | ushort key = findDoubleMedial(vowel: (HangulMedialIndex)VIndex); |
| 78 | if (key) { |
| 79 | HangulMedialIndex VIndexA, VIndexB; |
| 80 | unpackDoubleMedial(key, a&: VIndexA, b&: VIndexB); |
| 81 | result.append(c: QChar(VBase + (int)VIndexA)); |
| 82 | result.append(c: QChar(VBase + (int)VIndexB)); |
| 83 | } else { |
| 84 | result.append(c: QChar(VBase + VIndex)); |
| 85 | } |
| 86 | |
| 87 | // Decompose final consonant and check if it consists of double Jamo |
| 88 | int TIndex = SIndex % TCount; |
| 89 | if (TIndex != 0) { |
| 90 | key = findDoubleFinal(consonant: (HangulFinalIndex)TIndex); |
| 91 | if (key) { |
| 92 | HangulFinalIndex TIndexA, TIndexB; |
| 93 | unpackDoubleFinal(key, a&: TIndexA, b&: TIndexB); |
| 94 | result.append(c: QChar(finals[(int)TIndexA])); |
| 95 | result.append(c: QChar(finals[(int)TIndexB])); |
| 96 | } else { |
| 97 | result.append(c: QChar(finals[TIndex])); |
| 98 | } |
| 99 | } |
| 100 | } else { |
| 101 | result.append(c: ch); |
| 102 | } |
| 103 | } |
| 104 | return result; |
| 105 | } |
| 106 | |
| 107 | QString Hangul::compose(const QString &source) |
| 108 | { |
| 109 | const int len = source.length(); |
| 110 | if (len == 0) |
| 111 | return QString(); |
| 112 | |
| 113 | // Always add the initial character into buffer. |
| 114 | // The last character will serve as the current |
| 115 | // Hangul Syllable. |
| 116 | QChar last = source.at(i: 0); |
| 117 | QString result = QString(last); |
| 118 | |
| 119 | // Go through the input buffer starting at next character |
| 120 | for (int i = 1; i < len; i++) { |
| 121 | const QChar ch = source.at(i); |
| 122 | |
| 123 | // Check to see if the character is Hangul Compatibility Jamo |
| 124 | const ushort unicode = ch.unicode(); |
| 125 | if (isJamo(unicode)) { |
| 126 | |
| 127 | // Check to see if the character is syllable |
| 128 | const ushort lastUnicode = last.unicode(); |
| 129 | int SIndex = (int)lastUnicode - SBase; |
| 130 | if (SIndex >= 0 && SIndex < SCount) { |
| 131 | |
| 132 | // Check to see if the syllable type is LV or LV+T |
| 133 | int TIndex = SIndex % TCount; |
| 134 | if (TIndex == 0) { |
| 135 | |
| 136 | // If the current character is final consonant, then |
| 137 | // make syllable of form LV+T |
| 138 | TIndex = finals.indexOf(t: unicode); |
| 139 | if (TIndex != -1) { |
| 140 | last = QChar((int)lastUnicode + TIndex); |
| 141 | result.replace(i: result.length() - 1, len: 1, after: last); |
| 142 | continue; |
| 143 | } |
| 144 | |
| 145 | // Check to see if the current character is vowel |
| 146 | HangulMedialIndex VIndexB = (HangulMedialIndex)((int)unicode - VBase); |
| 147 | if (isMedial(vowel: VIndexB)) { |
| 148 | |
| 149 | // Some medial Jamos do not exist in the keyboard layout as is. |
| 150 | // Such Jamos can only be formed by combining the two specific Jamos, |
| 151 | // aka the double Jamos. |
| 152 | |
| 153 | HangulMedialIndex VIndexA = (HangulMedialIndex)((SIndex % NCount) / TCount); |
| 154 | if (isMedial(vowel: VIndexA)) { |
| 155 | |
| 156 | // Search the double medial map if such a combination exists |
| 157 | ushort key = packDoubleMedial(a: VIndexA, b: VIndexB); |
| 158 | const auto it = doubleMedialMap.constFind(akey: key); |
| 159 | if (it != doubleMedialMap.cend()) { |
| 160 | |
| 161 | // Update syllable by adding the difference between |
| 162 | // the vowels indices |
| 163 | HangulMedialIndex VIndexD = it.value(); |
| 164 | int VDiff = (int)VIndexD - (int)VIndexA; |
| 165 | last = QChar((int)lastUnicode + VDiff * TCount); |
| 166 | result.replace(i: result.length() - 1, len: 1, after: last); |
| 167 | continue; |
| 168 | } |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | } else { |
| 173 | |
| 174 | // Check to see if current jamo is vowel |
| 175 | int VIndex = (int)unicode - VBase; |
| 176 | if (VIndex >= 0 && VIndex < VCount) { |
| 177 | |
| 178 | // Since some initial and final consonants use the same |
| 179 | // Unicode values, we need to check whether the previous final |
| 180 | // Jamo is actually an initial Jamo of the next syllable. |
| 181 | // |
| 182 | // Consider the following scenario: |
| 183 | // LVT+V == not possible |
| 184 | // LV, L+V == possible |
| 185 | int LIndex = initials.indexOf(t: finals[TIndex]); |
| 186 | if (LIndex >= 0 && LIndex < LCount) { |
| 187 | |
| 188 | // Remove the previous final jamo from the syllable, |
| 189 | // making the current syllable of form LV |
| 190 | last = QChar((int)lastUnicode - TIndex); |
| 191 | result.replace(i: result.length() - 1, len: 1, after: last); |
| 192 | |
| 193 | // Make new syllable of form LV |
| 194 | last = QChar(SBase + (LIndex * VCount + VIndex) * TCount); |
| 195 | result.append(c: last); |
| 196 | continue; |
| 197 | } |
| 198 | |
| 199 | // Check to see if the current final Jamo is double consonant. |
| 200 | // In this scenario, the double consonant is split into parts |
| 201 | // and the second part is removed from the current syllable. |
| 202 | // Then the second part is joined with the current vowel making |
| 203 | // the new syllable of form LV. |
| 204 | ushort key = findDoubleFinal(consonant: (HangulFinalIndex)TIndex); |
| 205 | if (key) { |
| 206 | |
| 207 | // Split the consonant into two jamos and remove the |
| 208 | // second jamo B from the current syllable |
| 209 | HangulFinalIndex TIndexA, TIndexB; |
| 210 | unpackDoubleFinal(key, a&: TIndexA, b&: TIndexB); |
| 211 | last = QChar((int)lastUnicode - TIndex + (int)TIndexA); |
| 212 | result.replace(i: result.length() - 1, len: 1, after: last); |
| 213 | |
| 214 | // Add new syllable by combining the initial jamo |
| 215 | // and the current vowel |
| 216 | LIndex = initials.indexOf(t: finals[TIndexB]); |
| 217 | last = QChar(SBase + (LIndex * VCount + VIndex) * TCount); |
| 218 | result.append(c: last); |
| 219 | continue; |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | // Check whether the current consonant can connect to current |
| 224 | // consonant forming a double final consonant |
| 225 | HangulFinalIndex TIndexA = (HangulFinalIndex)TIndex; |
| 226 | if (isFinal(consonant: TIndexA)) { |
| 227 | |
| 228 | HangulFinalIndex TIndexB = (HangulFinalIndex)finals.indexOf(t: unicode); |
| 229 | if (isFinal(consonant: TIndexB)) { |
| 230 | |
| 231 | // Search the double final map if such a combination exists |
| 232 | ushort key = packDoubleFinal(a: TIndexA, b: TIndexB); |
| 233 | const auto it = doubleFinalMap.constFind(akey: key); |
| 234 | if (it != doubleFinalMap.cend()) { |
| 235 | |
| 236 | // Update syllable by adding the difference between |
| 237 | // the consonant indices |
| 238 | HangulFinalIndex TIndexD = it.value(); |
| 239 | int TDiff = (int)TIndexD - (int)TIndexA; |
| 240 | last = QChar((int)lastUnicode + TDiff); |
| 241 | result.replace(i: result.length() - 1, len: 1, after: last); |
| 242 | continue; |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | } else { |
| 249 | |
| 250 | // The last character is not syllable. |
| 251 | // Check to see if the last character is an initial consonant |
| 252 | int LIndex = initials.indexOf(t: lastUnicode); |
| 253 | if (LIndex != -1) { |
| 254 | |
| 255 | // If the current character is medial vowel, |
| 256 | // make syllable of form LV |
| 257 | int VIndex = (int)unicode - VBase; |
| 258 | if (VIndex >= 0 && VIndex < VCount) { |
| 259 | last = QChar(SBase + (LIndex * VCount + VIndex) * TCount); |
| 260 | result.replace(i: result.length() - 1, len: 1, after: last); |
| 261 | continue; |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | // Otherwise, add the character into buffer |
| 269 | last = ch; |
| 270 | result = result.append(c: ch); |
| 271 | } |
| 272 | return result; |
| 273 | } |
| 274 | |
| 275 | bool Hangul::isJamo(const ushort &unicode) |
| 276 | { |
| 277 | return unicode >= 0x3131 && unicode <= 0x3163; |
| 278 | } |
| 279 | |
| 280 | bool Hangul::isMedial(HangulMedialIndex vowel) |
| 281 | { |
| 282 | return vowel >= HANGUL_MEDIAL_A && vowel <= HANGUL_MEDIAL_I; |
| 283 | } |
| 284 | |
| 285 | bool Hangul::isFinal(HangulFinalIndex consonant) |
| 286 | { |
| 287 | return consonant >= HANGUL_FINAL_KIYEOK && consonant <= HANGUL_FINAL_HIEUH; |
| 288 | } |
| 289 | |
| 290 | ushort Hangul::findDoubleMedial(HangulMedialIndex vowel) |
| 291 | { |
| 292 | return doubleMedialMap.key(avalue: vowel, defaultKey: 0); |
| 293 | } |
| 294 | |
| 295 | ushort Hangul::findDoubleFinal(HangulFinalIndex consonant) |
| 296 | { |
| 297 | return doubleFinalMap.key(avalue: consonant, defaultKey: 0); |
| 298 | } |
| 299 | |
| 300 | // Packs two Hangul Jamo indices into 16-bit integer. |
| 301 | // The result can be used as a key to the double jamos lookup table. |
| 302 | // Note: The returned value is not a Unicode character! |
| 303 | ushort Hangul::packDoubleMedial(HangulMedialIndex a, HangulMedialIndex b) |
| 304 | { |
| 305 | Q_ASSERT(isMedial(a)); |
| 306 | Q_ASSERT(isMedial(b)); |
| 307 | return (ushort)a | ((ushort)b << 8); |
| 308 | } |
| 309 | |
| 310 | ushort Hangul::packDoubleFinal(HangulFinalIndex a, HangulFinalIndex b) |
| 311 | { |
| 312 | Q_ASSERT(isFinal(a)); |
| 313 | Q_ASSERT(isFinal(b)); |
| 314 | return (ushort)a | ((ushort)b << 8); |
| 315 | } |
| 316 | |
| 317 | void Hangul::unpackDoubleMedial(ushort key, HangulMedialIndex &a, HangulMedialIndex &b) |
| 318 | { |
| 319 | a = (HangulMedialIndex)(key & 0xFF); |
| 320 | b = (HangulMedialIndex)(key >> 8); |
| 321 | Q_ASSERT(isMedial(a)); |
| 322 | Q_ASSERT(isMedial(b)); |
| 323 | } |
| 324 | |
| 325 | void Hangul::unpackDoubleFinal(ushort key, HangulFinalIndex &a, HangulFinalIndex &b) |
| 326 | { |
| 327 | a = (HangulFinalIndex)(key & 0xFF); |
| 328 | b = (HangulFinalIndex)(key >> 8); |
| 329 | Q_ASSERT(isFinal(a)); |
| 330 | Q_ASSERT(isFinal(b)); |
| 331 | } |
| 332 | |
| 333 | QMap<ushort, Hangul::HangulMedialIndex> Hangul::initDoubleMedialMap() |
| 334 | { |
| 335 | QMap<ushort, HangulMedialIndex> map; |
| 336 | map.insert(akey: packDoubleMedial(a: HANGUL_MEDIAL_O, b: HANGUL_MEDIAL_A), avalue: HANGUL_MEDIAL_WA); |
| 337 | map.insert(akey: packDoubleMedial(a: HANGUL_MEDIAL_O, b: HANGUL_MEDIAL_AE), avalue: HANGUL_MEDIAL_WAE); |
| 338 | map.insert(akey: packDoubleMedial(a: HANGUL_MEDIAL_O, b: HANGUL_MEDIAL_I), avalue: HANGUL_MEDIAL_OE); |
| 339 | map.insert(akey: packDoubleMedial(a: HANGUL_MEDIAL_U, b: HANGUL_MEDIAL_EO), avalue: HANGUL_MEDIAL_WEO); |
| 340 | map.insert(akey: packDoubleMedial(a: HANGUL_MEDIAL_U, b: HANGUL_MEDIAL_E), avalue: HANGUL_MEDIAL_WE); |
| 341 | map.insert(akey: packDoubleMedial(a: HANGUL_MEDIAL_U, b: HANGUL_MEDIAL_I), avalue: HANGUL_MEDIAL_WI); |
| 342 | map.insert(akey: packDoubleMedial(a: HANGUL_MEDIAL_EU, b: HANGUL_MEDIAL_I), avalue: HANGUL_MEDIAL_YI); |
| 343 | return map; |
| 344 | } |
| 345 | |
| 346 | QMap<ushort, Hangul::HangulFinalIndex> Hangul::initDoubleFinalMap() |
| 347 | { |
| 348 | QMap<ushort, HangulFinalIndex> map; |
| 349 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_KIYEOK, b: HANGUL_FINAL_SIOS), avalue: HANGUL_FINAL_KIYEOK_SIOS); |
| 350 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_NIEUN, b: HANGUL_FINAL_CIEUC), avalue: HANGUL_FINAL_NIEUN_CIEUC); |
| 351 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_NIEUN, b: HANGUL_FINAL_HIEUH), avalue: HANGUL_FINAL_NIEUN_HIEUH); |
| 352 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_RIEUL, b: HANGUL_FINAL_KIYEOK), avalue: HANGUL_FINAL_RIEUL_KIYEOK); |
| 353 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_RIEUL, b: HANGUL_FINAL_MIEUM), avalue: HANGUL_FINAL_RIEUL_MIEUM); |
| 354 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_RIEUL, b: HANGUL_FINAL_PIEUP), avalue: HANGUL_FINAL_RIEUL_PIEUP); |
| 355 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_RIEUL, b: HANGUL_FINAL_SIOS), avalue: HANGUL_FINAL_RIEUL_SIOS); |
| 356 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_RIEUL, b: HANGUL_FINAL_THIEUTH), avalue: HANGUL_FINAL_RIEUL_THIEUTH); |
| 357 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_RIEUL, b: HANGUL_FINAL_PHIEUPH), avalue: HANGUL_FINAL_RIEUL_PHIEUPH); |
| 358 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_RIEUL, b: HANGUL_FINAL_HIEUH), avalue: HANGUL_FINAL_RIEUL_HIEUH); |
| 359 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_PIEUP, b: HANGUL_FINAL_SIOS), avalue: HANGUL_FINAL_PIEUP_SIOS); |
| 360 | map.insert(akey: packDoubleFinal(a: HANGUL_FINAL_SIOS, b: HANGUL_FINAL_SIOS), avalue: HANGUL_FINAL_SSANGSIOS); |
| 361 | return map; |
| 362 | } |
| 363 | |
| 364 | } // namespace QtVirtualKeyboard |
| 365 | QT_END_NAMESPACE |
| 366 | |