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 | |