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