1 // This file is part of Desktop App Toolkit,
2 // a set of libraries for developing nice desktop applications.
3 //
4 // For license and copyright information please follow this link:
5 // https://github.com/desktop-app/legal/blob/master/LEGAL
6 //
7 #include "codegen/emoji/generator.h"
8
9 #include <QtCore/QtPlugin>
10 #include <QtCore/QBuffer>
11 #include <QtGui/QFontDatabase>
12 #include <QtGui/QGuiApplication>
13 #include <QtGui/QPainter>
14 #include <QtCore/QDir>
15
16 #ifndef DESKTOP_APP_USE_PACKAGED
17 Q_IMPORT_PLUGIN(QWebpPlugin)
18 #ifdef Q_OS_MAC
19 Q_IMPORT_PLUGIN(QCocoaIntegrationPlugin)
20 #elif defined Q_OS_WIN
21 Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin)
22 #else // !Q_OS_MAC && !Q_OS_WIN
23 Q_IMPORT_PLUGIN(QXcbIntegrationPlugin)
24 #endif // !Q_OS_MAC && !Q_OS_WIN
25 #endif // DESKTOP_APP_USE_PACKAGED
26
27 namespace codegen {
28 namespace emoji {
29 namespace {
30
31 constexpr auto kErrorCantWritePath = 851;
32
33 constexpr auto kOriginalBits = 12;
34 constexpr auto kIdSizeBits = 6;
35
36 common::ProjectInfo Project = {
37 "codegen_emoji",
38 "empty",
39 false, // forceReGenerate
40 };
41
computeSourceRect(const QImage & image)42 QRect computeSourceRect(const QImage &image) {
43 auto size = image.width();
44 auto result = QRect(2, 2, size - 4, size - 4);
45 auto top = 1, bottom = 1, left = 1, right = 1;
46 auto rgbBits = reinterpret_cast<const QRgb*>(image.constBits());
47 for (auto i = 0; i != size; ++i) {
48 if (rgbBits[i] > 0) {
49 logDataError() << "Bad top border.";
50 return QRect();
51 } else if (rgbBits[(size - 1) * size + i] > 0) {
52 logDataError() << "Bad bottom border.";
53 return QRect();
54 } else if (rgbBits[i * size] > 0) {
55 logDataError() << "Bad left border.";
56 return QRect();
57 } else if (rgbBits[i * size + (size - 1)] > 0) {
58 logDataError() << "Bad right border.";
59 return QRect();
60 }
61 if (rgbBits[1 * size + i] > 0) {
62 top = -1;
63 } else if (top > 0 && rgbBits[2 * size + i] > 0) {
64 top = 0;
65 }
66 if (rgbBits[(size - 2) * size + i] > 0) {
67 bottom = -1;
68 } else if (bottom > 0 && rgbBits[(size - 3) * size + i] > 0) {
69 bottom = 0;
70 }
71 if (rgbBits[i * size + 1] > 0) {
72 left = -1;
73 } else if (left > 0 && rgbBits[i * size + 2] > 0) {
74 left = 0;
75 }
76 if (rgbBits[i * size + (size - 2)] > 0) {
77 right = -1;
78 } else if (right > 0 && rgbBits[i * size + (size - 3)] > 0) {
79 right = 0;
80 }
81 }
82 if (top < 0) {
83 if (bottom <= 0) {
84 logDataError() << "Bad vertical :(";
85 return QRect();
86 } else {
87 result.setY(result.y() + 1);
88 }
89 } else if (bottom < 0) {
90 if (top <= 0) {
91 logDataError() << "Bad vertical :(";
92 return QRect();
93 } else {
94 result.setY(result.y() - 1);
95 }
96 }
97 if (left < 0) {
98 if (right <= 0) {
99 logDataError() << "Bad horizontal :(";
100 return QRect();
101 } else {
102 result.setX(result.x() + 1);
103 }
104 } else if (right < 0) {
105 if (left <= 0) {
106 logDataError() << "Bad horizontal :(";
107 return QRect();
108 } else {
109 result.setX(result.x() - 1);
110 }
111 }
112 return result;
113 }
114
115 uint32 Crc32Table[256];
116 class Crc32Initializer {
117 public:
Crc32Initializer()118 Crc32Initializer() {
119 uint32 poly = 0x04C11DB7U;
120 for (auto i = 0; i != 256; ++i) {
121 Crc32Table[i] = reflect(i, 8) << 24;
122 for (auto j = 0; j != 8; ++j) {
123 Crc32Table[i] = (Crc32Table[i] << 1) ^ (Crc32Table[i] & (1 << 31) ? poly : 0);
124 }
125 Crc32Table[i] = reflect(Crc32Table[i], 32);
126 }
127 }
128
129 private:
reflect(uint32 val,char ch)130 uint32 reflect(uint32 val, char ch) {
131 uint32 result = 0;
132 for (int i = 1; i < (ch + 1); ++i) {
133 if (val & 1) {
134 result |= 1 << (ch - i);
135 }
136 val >>= 1;
137 }
138 return result;
139 }
140
141 };
142
countCrc32(const void * data,std::size_t size)143 uint32 countCrc32(const void *data, std::size_t size) {
144 static Crc32Initializer InitTable;
145
146 auto buffer = static_cast<const unsigned char*>(data);
147 auto result = uint32(0xFFFFFFFFU);
148 for (auto i = std::size_t(0); i != size; ++i) {
149 result = (result >> 8) ^ Crc32Table[(result & 0xFFU) ^ buffer[i]];
150 }
151 return (result ^ 0xFFFFFFFFU);
152 }
153
154 } // namespace
155
Generator(const Options & options)156 Generator::Generator(const Options &options) : project_(Project)
157 , writeImages_(options.writeImages)
158 , data_(PrepareData(options.dataPath, options.oldDataPaths))
159 , replaces_(PrepareReplaces(options.replacesPath)) {
160 QDir dir(options.outputPath);
161 if (!dir.mkpath(".")) {
162 common::logError(kErrorCantWritePath, "Command Line") << "can not open path for writing: " << dir.absolutePath().toStdString();
163 data_ = Data();
164 }
165 if (!CheckAndConvertReplaces(replaces_, data_)) {
166 replaces_ = Replaces(replaces_.filename);
167 }
168
169 outputPath_ = dir.absolutePath() + "/emoji";
170 spritePath_ = dir.absolutePath() + "/emoji";
171 suggestionsPath_ = dir.absolutePath() + "/emoji_suggestions_data";
172 }
173
generate()174 int Generator::generate() {
175 if (data_.list.empty() || replaces_.list.isEmpty()) {
176 return -1;
177 } else if (!writeImages_.isEmpty()) {
178 return writeImages() ? 0 : -1;
179 } else if (!writeSource()
180 || !writeHeader()
181 || !writeSuggestionsSource()
182 || !writeSuggestionsHeader()
183 || !common::TouchTimestamp(outputPath_)) {
184 return -1;
185 }
186 return 0;
187 }
188
189 constexpr auto kEmojiInRow = 32;
190 constexpr auto kEmojiRowsInFile = 16;
191 constexpr auto kEmojiQuality = 99;
192 constexpr auto kEmojiSize = 72;
193 constexpr auto kEmojiFontSize = 72;
194 constexpr auto kEmojiShiftTop = 67 - 4;
195 constexpr auto kScaleFromLarge = true;
196 constexpr auto kLargeEmojiSize = 180;
197 constexpr auto kLargeEmojiFontSizeMac = 180;
198 constexpr auto kLargeEmojiShiftTopMac = 167 - 9;
199 constexpr auto kEmojiShiftLeftMac = 0;
200 constexpr auto kLargeEmojiFontSizeAndroid = 178;
201 constexpr auto kLargeEmojiShiftTopAndroid = 140;
202 constexpr auto kEmojiShiftLeftAndroid = -4;
203
204 enum class ImageType {
205 Mac,
206 Android,
207 Twemoji,
208 JoyPixels,
209 };
210
GuessImageType(QString tag)211 [[nodiscard]] ImageType GuessImageType(QString tag) {
212 if (tag.indexOf("NotoColorEmoji") >= 0) {
213 return ImageType::Android;
214 } else if (tag.indexOf("twemoji") >= 0) {
215 return ImageType::Twemoji;
216 } else if (tag.indexOf("joypixels") >= 0) {
217 return ImageType::JoyPixels;
218 }
219 return ImageType::Mac;
220 }
221
PaintSingleFromFont(QPainter & p,QRect targetRect,const Emoji & data,QFont & font,ImageType type,QImage & singleImage)222 bool PaintSingleFromFont(QPainter &p, QRect targetRect, const Emoji &data, QFont &font, ImageType type, QImage &singleImage) {
223 singleImage.fill(Qt::transparent);
224 {
225 QPainter q(&singleImage);
226 q.setPen(QColor(0, 0, 0, 255));
227 q.setFont(font);
228 const auto shiftTop = !kScaleFromLarge
229 ? kEmojiShiftTop
230 : (type == ImageType::Mac)
231 ? kLargeEmojiShiftTopMac
232 : kLargeEmojiShiftTopAndroid;
233 const auto shiftLeft = (type == ImageType::Mac)
234 ? kEmojiShiftLeftMac
235 : kEmojiShiftLeftAndroid;
236 auto text = data.id;
237 if (type == ImageType::Android) {
238 if (text.size() > 2 && text.indexOf(QChar(0xFE0F)) >= 0) {
239 // Some emoji, like "Kiss: Person, Person, Light Skin Tone, Medium Skin Tone",
240 // aren't rendered correctly if string still contains 0xFE0F-s from Apple.
241 text = text.replace(QChar(0xFE0F), QString());
242 }
243 }
244 q.drawText(2 + shiftLeft, 2 + shiftTop, text);
245 }
246 auto sourceRect = computeSourceRect(singleImage);
247 if (sourceRect.isEmpty()) {
248 std::cout << "Bad emoji: " << data.id.toStdString() << std::endl;
249 return false;
250 }
251 if (kScaleFromLarge) {
252 p.drawImage(targetRect, singleImage.copy(sourceRect).scaled(kEmojiSize, kEmojiSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
253 } else {
254 p.drawImage(targetRect, singleImage, sourceRect);
255 }
256 return true;
257 }
258
PaintSingleFromFile(QPainter & p,QRect targetRect,const Emoji & data,const QString & base,ImageType type)259 bool PaintSingleFromFile(QPainter &p, QRect targetRect, const Emoji &data, const QString &base, ImageType type) {
260 auto nameParts = QStringList();
261 auto namePartsFull = QStringList();
262 const auto ucsToPart = [&](uint32 ucs4) {
263 if (type == ImageType::Twemoji) {
264 return QString::number(ucs4, 16).toLower();
265 } else if (type == ImageType::JoyPixels) {
266 return QString("%1").arg(ucs4, 4, 16, QChar('0')).toLower();
267 }
268 return QString();
269 };
270 for (auto i = 0; i != data.id.size(); ++i) {
271 uint ucs4 = data.id[i].unicode();
272 if (type == ImageType::JoyPixels && ucs4 == 0x200d) {
273 continue;
274 } else if (ucs4 == kPostfix) {
275 namePartsFull.push_back(ucsToPart(ucs4));
276 continue;
277 } else if (QChar::isHighSurrogate(ucs4) && i + 1 != data.id.size()) {
278 ushort low = data.id[++i].unicode();
279 if (QChar::isLowSurrogate(low)) {
280 ucs4 = QChar::surrogateToUcs4(ucs4, low);
281 } else {
282 return false;
283 }
284 }
285 nameParts.push_back(ucsToPart(ucs4));
286 namePartsFull.push_back(ucsToPart(ucs4));
287 }
288 const auto fillEmpty = [&] {
289 const auto column = targetRect.x() / targetRect.width();
290 const auto row = targetRect.y() / targetRect.height();
291 p.fillRect(targetRect, ((column + row) % 2) ? QColor(255, 0, 0, 255) : QColor(0, 255, 0, 255));
292 };
293 auto checkNames = QStringList();
294 checkNames.push_back(nameParts.join('-'));
295 checkNames.push_back(namePartsFull.join('-'));
296 checkNames.push_back(nameParts.join('-') + "-fe0f");
297 if (type == ImageType::Twemoji) {
298 // Some names have 'fe0f' after a small joined part, while
299 // the data doesn't have it.
300 //
301 // Like, we have in data:
302 // 1f469-1f3fb-200d-2764-200d-1f468-1f3fc
303 // and twemoji has file
304 // 1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc
305 //
306 // But we need to check all combination, because we have:
307 // 1f469-1f3fb-200d-2764-200d-1f48b-200d-1f469-1f3fd
308 // and twemoji has file
309 // 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd
310 //
311 // So we add all the combinations.
312 auto joined = std::vector<QStringList>();
313 auto from = 0;
314 do {
315 auto sep = nameParts.indexOf("200d", from);
316 if (sep < 0) {
317 joined.push_back(nameParts.mid(from));
318 } else if (sep > from) {
319 joined.push_back(nameParts.mid(from, sep - from));
320 }
321 from = sep + 1;
322 } while (from > 0);
323 if (joined.size() > 0) {
324 auto smallIndices = std::vector<int>();
325 for (auto i = 0; i < int(joined.size()); ++i) {
326 if (joined[i].size() == 1) {
327 smallIndices.push_back(i);
328 }
329 }
330 if (!smallIndices.empty()) {
331 // Add all the combinations where some of the small
332 // joined groups have the postfix added and some don't.
333 const auto count = (1U << int(smallIndices.size()));
334 for (auto bits = 0U; bits != count; ++bits) {
335 auto partsWithPostfixes = QStringList();
336 auto smallIndex = 0;
337 for (auto i = 0; i < int(joined.size()); ++i) {
338 partsWithPostfixes.append(joined[i]);
339 if (joined[i].size() == 1) {
340 if (bits & (1U << smallIndex)) {
341 partsWithPostfixes.append("fe0f");
342 }
343 ++smallIndex;
344 }
345 if (i + 1 < int(joined.size())) {
346 partsWithPostfixes.append("200d");
347 }
348 }
349 checkNames.push_back(partsWithPostfixes.join('-'));
350 }
351 }
352 }
353 }
354 const auto image = [&] {
355 for (const auto &name : checkNames) {
356 if (const auto result = QImage(base + '/' + name + ".png"); !result.isNull()) {
357 return result;
358 }
359 }
360 return QImage();
361 }();
362 const auto allowDownscale = (type == ImageType::JoyPixels);
363 if (image.isNull()) {
364 std::cout << "NOT FOUND: " << checkNames[1].toStdString() << std::endl;
365 fillEmpty();
366 return false;
367 } else if (image.width() != image.height()
368 || image.width() < targetRect.width()
369 || image.height() < targetRect.height()
370 || (!allowDownscale && image.width() != targetRect.width())) {
371 std::cout << "BAD SIZE: " << checkNames[1].toStdString() << std::endl;
372 fillEmpty();
373 return false;
374 } else if (allowDownscale && image.width() > targetRect.width()) {
375 p.drawImage(targetRect, image.scaled(targetRect.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
376 } else {
377 p.drawImage(targetRect, image);
378 }
379 return true;
380 }
381
generateImage(int imageIndex)382 QImage Generator::generateImage(int imageIndex) {
383 const auto type = GuessImageType(writeImages_);
384
385 auto emojiCount = int(data_.list.size());
386 auto columnsCount = kEmojiInRow;
387
388 auto sourceSize = kScaleFromLarge ? kLargeEmojiSize : kEmojiSize;
389
390 auto font = QGuiApplication::font();
391 auto base = writeImages_;
392 if (type == ImageType::Android) {
393 const auto regularId = QFontDatabase::addApplicationFont(base);
394 if (regularId < 0) {
395 std::cout << "NotoColorEmoji.ttf not loaded from: " << base.toStdString() << std::endl;
396 return QImage();
397 }
398 } else if (type == ImageType::Twemoji) {
399 base += "/assets/72x72";
400 } else if (type == ImageType::JoyPixels) {
401 base += "/png/unicode/512";
402 }
403 if (type == ImageType::Mac || type == ImageType::Android) {
404 const auto family = (type == ImageType::Mac)
405 ? QStringLiteral("Apple Color Emoji")
406 : QStringLiteral("Noto Color Emoji");
407 font.setFamily(family);
408 font.setPixelSize(!kScaleFromLarge
409 ? kEmojiFontSize
410 : (type == ImageType::Mac)
411 ? kLargeEmojiFontSizeMac
412 : kLargeEmojiFontSizeAndroid);
413 if (QFontInfo(font).family() != family) {
414 return QImage();
415 }
416 } else if (type == ImageType::Twemoji || type == ImageType::JoyPixels) {
417 if (!QDir(base).exists()) {
418 return QImage();
419 }
420 } else {
421 return QImage();
422 }
423
424 auto singleSize = 4 + sourceSize;
425 const auto inFileShift = (imageIndex * kEmojiInRow * kEmojiRowsInFile);
426 if (inFileShift >= emojiCount) {
427 return QImage();
428 }
429 const auto maxInFile = emojiCount - inFileShift;
430 const auto inFileCount = std::min(maxInFile, kEmojiInRow * kEmojiRowsInFile);
431 auto rowsCount = (inFileCount / columnsCount) + ((inFileCount % columnsCount) ? 1 : 0);
432 auto emojiImage = QImage(columnsCount * kEmojiSize, rowsCount * kEmojiSize, QImage::Format_ARGB32);
433 emojiImage.fill(Qt::transparent);
434 auto skippedCount = 0;
435 auto singleImage = QImage(singleSize, singleSize, QImage::Format_ARGB32);
436 {
437 QPainter p(&emojiImage);
438 p.setRenderHint(QPainter::SmoothPixmapTransform);
439
440 auto column = 0;
441 auto row = 0;
442 for (auto i = 0; i != inFileCount; ++i) {
443 auto &emoji = data_.list[inFileShift + i];
444 const auto targetRect = QRect(column * kEmojiSize, row * kEmojiSize, kEmojiSize, kEmojiSize);
445 if (type == ImageType::Mac || type == ImageType::Android) {
446 if (!PaintSingleFromFont(p, targetRect, emoji, font, type, singleImage)) {
447 ++skippedCount;
448 }
449 } else if (type == ImageType::Twemoji || type == ImageType::JoyPixels) {
450 if (!PaintSingleFromFile(p, targetRect, emoji, base, type)) {
451 ++skippedCount;
452 }
453 }
454 ++column;
455 if (column == columnsCount) {
456 column = 0;
457 ++row;
458 }
459 }
460 }
461 return (skippedCount > 0) ? QImage() : emojiImage;
462 }
463
writeImages()464 bool Generator::writeImages() {
465 auto badCount = 0;
466 auto imageIndex = 0;
467 while (imageIndex * kEmojiInRow * kEmojiRowsInFile < data_.list.size()) {
468 auto image = generateImage(imageIndex);
469 if (image.isNull()) {
470 ++badCount;
471 ++imageIndex;
472 continue;
473 }
474 auto postfix = '_' + QString::number(imageIndex + 1);
475 auto filename = spritePath_ + postfix + ".webp";
476 auto bytes = QByteArray();
477 {
478 QBuffer buffer(&bytes);
479 if (!image.save(&buffer, "WEBP", kEmojiQuality)) {
480 logDataError() << "Could not save 'emoji" << postfix.toStdString() << ".webp'.";
481 return false;
482 }
483 }
484 auto needResave = !QFileInfo::exists(filename);
485 if (!needResave) {
486 QFile file(filename);
487 if (!file.open(QIODevice::ReadOnly)) {
488 needResave = true;
489 } else {
490 auto already = file.readAll();
491 if (already.size() != bytes.size() || memcmp(already.constData(), bytes.constData(), already.size())) {
492 needResave = true;
493 }
494 }
495 }
496 if (needResave) {
497 QFile file(filename);
498 if (!file.open(QIODevice::WriteOnly)) {
499 logDataError() << "Could not open 'emoji" << postfix.toStdString() << ".webp'.";
500 return false;
501 } else {
502 if (file.write(bytes) != bytes.size()) {
503 logDataError() << "Could not write 'emoji" << postfix.toStdString() << ".webp'.";
504 return false;
505 }
506 }
507 }
508 ++imageIndex;
509 }
510 return (badCount == 0);
511 }
512
writeSource()513 bool Generator::writeSource() {
514 source_ = std::make_unique<common::CppFile>(outputPath_ + ".cpp", project_);
515
516 source_->include("emoji_suggestions_data.h").include("ui/emoji_config.h").newline();
517 source_->pushNamespace("Ui").pushNamespace("Emoji").pushNamespace();
518 source_->stream() << "\
519 \n\
520 std::vector<One> Items;\n\
521 \n";
522 if (!writeInitCode()) {
523 return false;
524 }
525 if (!writeSections()) {
526 return false;
527 }
528 if (!writeFindReplace()) {
529 return false;
530 }
531 if (!writeFind()) {
532 return false;
533 }
534 source_->popNamespace().newline().pushNamespace("internal");
535 source_->stream() << "\
536 \n\
537 int FullCount() {\n\
538 return Items.size();\n\
539 }\n\
540 \n\
541 EmojiPtr ByIndex(int index) {\n\
542 return (index >= 0 && index < Items.size()) ? &Items[index] : nullptr;\n\
543 }\n\
544 \n\
545 EmojiPtr FindReplace(const QChar *start, const QChar *end, int *outLength) {\n\
546 auto index = FindReplaceIndex(start, end, outLength);\n\
547 return index ? &Items[index - 1] : nullptr;\n\
548 }\n\
549 \n\
550 const std::vector<std::pair<QString, int>> GetReplacementPairs() {\n\
551 return ReplacementPairs;\n\
552 }\n\
553 \n\
554 EmojiPtr Find(const QChar *start, const QChar *end, int *outLength) {\n\
555 auto index = FindIndex(start, end, outLength);\n\
556 return index ? &Items[index - 1] : nullptr;\n\
557 }\n\
558 \n\
559 void Init() {\n\
560 auto id = IdData;\n\
561 auto takeString = [&id](int size) {\n\
562 auto result = QString::fromRawData(reinterpret_cast<const QChar*>(id), size);\n\
563 id += size;\n\
564 return result;\n\
565 };\n\
566 \n\
567 Items.reserve(base::array_size(Data));\n\
568 for (auto &data : Data) {\n\
569 Items.emplace_back(\n\
570 takeString(data.idSize),\n\
571 data.original ? &Items[data.original - 1] : nullptr,\n\
572 uint32(Items.size()),\n\
573 data.postfixed ? true : false,\n\
574 data.variated ? true : false,\n\
575 One::CreationTag());\n\
576 }\n\
577 InitReplacements();\n\
578 }\n\
579 \n";
580 source_->popNamespace();
581
582 if (!writeGetSections()) {
583 return false;
584 }
585
586 return source_->finalize();
587 }
588
writeHeader()589 bool Generator::writeHeader() {
590 auto header = std::make_unique<common::CppFile>(outputPath_ + ".h", project_);
591 header->includeFromLibrary("QtCore/QChar");
592 header->includeFromLibrary("QtCore/QString");
593 header->includeFromLibrary("QtCore/QVector");
594 header->newline();
595 header->includeFromLibrary("vector");
596 header->newline();
597
598 header->pushNamespace("Ui").pushNamespace("Emoji");
599 header->stream() << "class One;\n";
600 header->popNamespace().popNamespace().newline();
601
602 header->stream() << "\
603 using EmojiPtr = const Ui::Emoji::One*;\n\
604 using EmojiPack = QVector<EmojiPtr>;\n\
605 \n";
606
607 header->pushNamespace("Ui").pushNamespace("Emoji").pushNamespace("internal");
608 header->stream() << "\
609 \n\
610 void Init();\n\
611 \n\
612 int FullCount();\n\
613 EmojiPtr ByIndex(int index);\n\
614 \n\
615 EmojiPtr Find(const QChar *ch, const QChar *end, int *outLength = nullptr);\n\
616 \n\
617 const std::vector<std::pair<QString, int>> GetReplacementPairs();\n\
618 EmojiPtr FindReplace(const QChar *ch, const QChar *end, int *outLength = nullptr);\n\
619 \n";
620 header->popNamespace().stream() << "\
621 \n\
622 constexpr auto kPostfix = static_cast<ushort>(0xFE0F);\n\
623 \n\
624 enum class Section {\n\
625 Recent,\n\
626 People,\n\
627 Nature,\n\
628 Food,\n\
629 Activity,\n\
630 Travel,\n\
631 Objects,\n\
632 Symbols,\n\
633 };\n\
634 \n\
635 int GetSectionCount(Section section);\n\
636 QVector<const One*> GetSection(Section section);\n\
637 \n";
638 return header->finalize();
639 }
640
641 template <typename Callback>
enumerateWholeList(Callback callback)642 bool Generator::enumerateWholeList(Callback callback) {
643 auto index = 0;
644 auto variated = -1;
645 auto coloredCount = 0;
646 for (auto &item : data_.list) {
647 if (!callback(item.id, item.postfixed, item.variated, item.colored, variated)) {
648 return false;
649 }
650 if (coloredCount > 0 && (item.variated || !item.colored)) {
651 if (!colorsCount_) {
652 colorsCount_ = coloredCount;
653 } else if (colorsCount_ != coloredCount) {
654 logDataError() << "different colored emoji count exist.";
655 return false;
656 }
657 coloredCount = 0;
658 }
659 if (item.variated) {
660 variated = index;
661 } else if (item.colored) {
662 if (variated <= 0) {
663 logDataError() << "wrong order of colored items.";
664 return false;
665 }
666 ++coloredCount;
667 } else if (variated >= 0) {
668 variated = -1;
669 }
670 ++index;
671 }
672 return true;
673 }
674
writeInitCode()675 bool Generator::writeInitCode() {
676 source_->stream() << "\
677 struct DataStruct {\n\
678 uint32 original : " << kOriginalBits << ";\n\
679 uint32 idSize : " << kIdSizeBits << ";\n\
680 uint32 postfixed : 1;\n\
681 uint32 variated : 1;\n\
682 };\n\
683 \n\
684 const ushort IdData[] = {";
685 startBinary();
686 if (!enumerateWholeList([this](Id id, bool isPostfixed, bool isVariated, bool isColored, int original) {
687 return writeStringBinary(source_.get(), id);
688 })) {
689 return false;
690 }
691 if (_binaryFullLength >= std::numeric_limits<ushort>::max()) {
692 logDataError() << "Too many IdData elements.";
693 return false;
694 }
695 source_->stream() << " };\n\
696 \n\
697 const DataStruct Data[] = {\n";
698 if (!enumerateWholeList([this](Id id, bool isPostfixed, bool isVariated, bool isColored, int original) {
699 if (original + 1 >= (1 << kOriginalBits)) {
700 logDataError() << "Too many entries.";
701 return false;
702 }
703 if (id.size() >= (1 << kIdSizeBits)) {
704 logDataError() << "Too large id.";
705 return false;
706 }
707 source_->stream() << "\
708 { uint32(" << (isColored ? (original + 1) : 0) << "), uint32(" << id.size() << "), uint32(" << (isPostfixed ? "1" : "0") << "), uint32(" << (isVariated ? "1" : "0") << ") },\n";
709 return true;
710 })) {
711 return false;
712 }
713
714 source_->stream() << "\
715 };\n";
716
717 return true;
718 }
719
writeSections()720 bool Generator::writeSections() {
721 source_->stream() << "\
722 const ushort SectionData[] = {";
723 startBinary();
724 for (auto &category : data_.categories) {
725 for (auto index : category) {
726 writeIntBinary(source_.get(), index);
727 }
728 }
729 source_->stream() << " };\n\
730 \n\
731 EmojiPack FillSection(int offset, int size) {\n\
732 auto result = EmojiPack();\n\
733 result.reserve(size);\n\
734 for (auto index : gsl::make_span(SectionData + offset, size)) {\n\
735 result.push_back(&Items[index]);\n\
736 }\n\
737 return result;\n\
738 }\n\n";
739 return true;
740 }
741
writeGetSections()742 bool Generator::writeGetSections() {
743 constexpr const char *sectionNames[] = {
744 "Section::People",
745 "Section::Nature",
746 "Section::Food",
747 "Section::Activity",
748 "Section::Travel",
749 "Section::Objects",
750 "Section::Symbols",
751 };
752 source_->stream() << "\
753 \n\
754 int GetSectionCount(Section section) {\n\
755 Expects(section != Section::Recent);\n\
756 \n\
757 switch (section) {\n";
758 auto countIndex = 0;
759 for (auto name : sectionNames) {
760 if (countIndex >= int(data_.categories.size())) {
761 logDataError() << "category " << countIndex << " not found.";
762 return false;
763 }
764 source_->stream() << "\
765 case " << name << ": return " << data_.categories[countIndex++].size() << ";\n";
766 }
767 source_->stream() << "\
768 }\n\
769 return 0;\n\
770 }\n\
771 \n\
772 EmojiPack GetSection(Section section) {\n\
773 Expects(section != Section::Recent);\n\
774 \n\
775 switch (section) {\n";
776 auto index = 0;
777 auto offset = 0;
778 for (auto name : sectionNames) {
779 if (index >= int(data_.categories.size())) {
780 logDataError() << "category " << index << " not found.";
781 return false;
782 }
783 auto &category = data_.categories[index++];
784 source_->stream() << "\
785 \n\
786 case " << name << ": {\n\
787 static auto result = FillSection(" << offset << ", " << category.size() << ");\n\
788 return result;\n\
789 } break;\n";
790 offset += category.size();
791 }
792 source_->stream() << "\
793 }\n\
794 return EmojiPack();\n\
795 }\n\
796 \n";
797 return true;
798 }
799
writeFindReplace()800 bool Generator::writeFindReplace() {
801 source_->stream() << "\
802 \n\
803 const std::vector<std::pair<QString, int>> ReplacementPairs = {\n";
804 for (const auto &[what, index] : data_.replaces) {
805 source_->stream() << "\
806 { \"" << what << "\", " << index << " },\n";
807 }
808 source_->stream() << "\
809 };\n\
810 \n\
811 int FindReplaceIndex(const QChar *start, const QChar *end, int *outLength) {\n\
812 auto ch = start;\n\
813 \n";
814
815 if (!writeFindFromDictionary(data_.replaces)) {
816 return false;
817 }
818
819 source_->stream() << "\
820 }\n";
821
822 return true;
823 }
824
writeFind()825 bool Generator::writeFind() {
826 source_->stream() << "\
827 \n\
828 int FindIndex(const QChar *start, const QChar *end, int *outLength) {\n\
829 auto ch = start;\n\
830 \n";
831
832 if (!writeFindFromDictionary(data_.map, true, data_.postfixRequired)) {
833 return false;
834 }
835
836 source_->stream() << "\
837 }\n\
838 \n";
839
840 return true;
841 }
842
writeFindFromDictionary(const std::map<QString,int,std::greater<QString>> & dictionary,bool skipPostfixes,const std::set<int> & postfixRequired)843 bool Generator::writeFindFromDictionary(
844 const std::map<QString, int, std::greater<QString>> &dictionary,
845 bool skipPostfixes,
846 const std::set<int> &postfixRequired) {
847 auto tabs = [](int size) {
848 return QString(size, '\t');
849 };
850
851 std::map<int, int> uniqueFirstChars;
852 auto foundMax = 0, foundMin = 65535;
853 for (auto &item : dictionary) {
854 auto ch = item.first[0].unicode();
855 if (foundMax < ch) foundMax = ch;
856 if (foundMin > ch) foundMin = ch;
857 uniqueFirstChars[ch] = 0;
858 }
859
860 enum class UsedCheckType {
861 Switch,
862 If,
863 };
864 auto checkTypes = QVector<UsedCheckType>();
865 auto chars = QString();
866 auto tabsUsed = 1;
867 auto lengthsCounted = std::set<QString>();
868
869 auto writeSkipPostfix = [this, &tabs, skipPostfixes](int tabsCount) {
870 if (skipPostfixes) {
871 source_->stream() << tabs(tabsCount) << "if (++ch != end && ch->unicode() == kPostfix) ++ch;\n";
872 } else {
873 source_->stream() << tabs(tabsCount) << "++ch;\n";
874 }
875 };
876
877 // Returns true if at least one check was finished.
878 auto finishChecksTillKey = [this, &chars, &checkTypes, &tabsUsed, tabs](const QString &key) {
879 auto result = false;
880 while (!chars.isEmpty() && !key.startsWith(chars)) {
881 result = true;
882
883 auto wasType = checkTypes.back();
884 chars.resize(chars.size() - 1);
885 checkTypes.pop_back();
886 if (wasType == UsedCheckType::Switch || wasType == UsedCheckType::If) {
887 --tabsUsed;
888 if (wasType == UsedCheckType::Switch) {
889 source_->stream() << tabs(tabsUsed) << "break;\n";
890 }
891 if ((!chars.isEmpty() && !key.startsWith(chars)) || key == chars) {
892 source_->stream() << tabs(tabsUsed) << "}\n";
893 }
894 }
895 }
896 return result;
897 };
898
899 // Check if we can use "if" for a check on "charIndex" in "it" (otherwise only "switch")
900 auto canUseIfForCheck = [](auto it, auto end, int charIndex) {
901 auto key = it->first;
902 auto i = it;
903 auto keyStart = key.mid(0, charIndex);
904 for (++i; i != end; ++i) {
905 auto nextKey = i->first;
906 if (nextKey.mid(0, charIndex) != keyStart) {
907 return true;
908 } else if (nextKey.size() > charIndex && nextKey[charIndex] != key[charIndex]) {
909 return false;
910 }
911 }
912 return true;
913 };
914
915 for (auto i = dictionary.cbegin(), e = dictionary.cend(); i != e; ++i) {
916 auto &item = *i;
917 auto key = item.first;
918 auto weContinueOldSwitch = finishChecksTillKey(key);
919 while (chars.size() != key.size()) {
920 auto checking = chars.size();
921 auto partialKey = key.mid(0, checking);
922 if (dictionary.find(partialKey) != dictionary.cend()) {
923 if (lengthsCounted.find(partialKey) == end(lengthsCounted)) {
924 lengthsCounted.emplace(partialKey);
925 source_->stream() << tabs(tabsUsed) << "if (outLength) *outLength = (ch - start);\n";
926 }
927 }
928
929 auto keyChar = key[checking];
930 auto keyCharString = "0x" + QString::number(keyChar.unicode(), 16);
931 auto usedIfForCheck = !weContinueOldSwitch && canUseIfForCheck(i, e, checking);
932 if (weContinueOldSwitch) {
933 weContinueOldSwitch = false;
934 } else if (!usedIfForCheck) {
935 source_->stream() << tabs(tabsUsed) << "if (ch != end) switch (ch->unicode()) {\n";
936 }
937 if (usedIfForCheck) {
938 source_->stream() << tabs(tabsUsed) << "if (ch != end && ch->unicode() == " << keyCharString << ") {\n";
939 checkTypes.push_back(UsedCheckType::If);
940 } else {
941 source_->stream() << tabs(tabsUsed) << "case " << keyCharString << ":\n";
942 checkTypes.push_back(UsedCheckType::Switch);
943 }
944 writeSkipPostfix(++tabsUsed);
945 chars.push_back(keyChar);
946 }
947
948 if (postfixRequired.find(item.second) != end(postfixRequired)) {
949 source_->stream() << tabs(tabsUsed) << "if ((ch - 1)->unicode() != kPostfix) {\n";
950 source_->stream() << tabs(tabsUsed + 1) << "return 0;\n";
951 source_->stream() << tabs(tabsUsed) << "}\n";
952 }
953 if (lengthsCounted.find(key) == end(lengthsCounted)) {
954 lengthsCounted.emplace(key);
955 source_->stream() << tabs(tabsUsed) << "if (outLength) *outLength = (ch - start);\n";
956 }
957
958 source_->stream() << tabs(tabsUsed) << "return " << (item.second + 1) << ";\n";
959 }
960 finishChecksTillKey(QString());
961
962 source_->stream() << "\
963 \n\
964 return 0;\n";
965 return true;
966 }
967
writeSuggestionsSource()968 bool Generator::writeSuggestionsSource() {
969 suggestionsSource_ = std::make_unique<common::CppFile>(suggestionsPath_ + ".cpp", project_);
970 suggestionsSource_->stream() << "\
971 #include <map>\n\
972 \n";
973 suggestionsSource_->pushNamespace("Ui").pushNamespace("Emoji").pushNamespace("internal").pushNamespace();
974 suggestionsSource_->stream() << "\
975 \n";
976 if (!writeReplacements()) {
977 return false;
978 }
979 suggestionsSource_->popNamespace().newline();
980 if (!writeGetReplacements()) {
981 return false;
982 }
983
984 return suggestionsSource_->finalize();
985 }
986
writeSuggestionsHeader()987 bool Generator::writeSuggestionsHeader() {
988 auto maxLength = 0;
989 for (auto &replace : replaces_.list) {
990 if (maxLength < replace.replacement.size()) {
991 maxLength = replace.replacement.size();
992 }
993 }
994 auto header = std::make_unique<common::CppFile>(suggestionsPath_ + ".h", project_);
995 header->include("emoji_suggestions.h").newline();
996 header->pushNamespace("Ui").pushNamespace("Emoji").pushNamespace("internal");
997 header->stream() << "\
998 \n\
999 struct Replacement {\n\
1000 utf16string emoji;\n\
1001 utf16string replacement;\n\
1002 std::vector<utf16string> words;\n\
1003 };\n\
1004 \n\
1005 constexpr auto kReplacementMaxLength = " << maxLength << ";\n\
1006 \n\
1007 void InitReplacements();\n\
1008 const std::vector<Replacement> &GetAllReplacements();\n\
1009 const std::vector<const Replacement*> *GetReplacements(utf16char first);\n\
1010 utf16string GetReplacementEmoji(utf16string replacement);\n\
1011 \n";
1012 return header->finalize();
1013 }
1014
writeReplacements()1015 bool Generator::writeReplacements() {
1016 QMap<QChar, QVector<int>> byCharIndices;
1017 suggestionsSource_->stream() << "\
1018 struct ReplacementStruct {\n\
1019 small emojiSize;\n\
1020 small replacementSize;\n\
1021 small wordsCount;\n\
1022 };\n\
1023 \n\
1024 const utf16char ReplacementData[] = {";
1025 startBinary();
1026 for (auto i = 0, size = int(replaces_.list.size()); i != size; ++i) {
1027 auto &replace = replaces_.list[i];
1028 if (!writeStringBinary(suggestionsSource_.get(), replace.id)) {
1029 return false;
1030 }
1031 if (!writeStringBinary(suggestionsSource_.get(), replace.replacement)) {
1032 return false;
1033 }
1034 for (auto &word : replace.words) {
1035 if (!writeStringBinary(suggestionsSource_.get(), word)) {
1036 return false;
1037 }
1038 auto &index = byCharIndices[word[0]];
1039 if (index.isEmpty() || index.back() != i) {
1040 index.push_back(i);
1041 }
1042 }
1043 }
1044 suggestionsSource_->stream() << " };\n\
1045 \n\
1046 const small ReplacementWordLengths[] = {";
1047 startBinary();
1048 for (auto &replace : replaces_.list) {
1049 auto wordLengths = QStringList();
1050 for (auto &word : replace.words) {
1051 writeIntBinary(suggestionsSource_.get(), word.size());
1052 }
1053 }
1054 suggestionsSource_->stream() << " };\n\
1055 \n\
1056 const ReplacementStruct ReplacementInitData[] = {\n";
1057 for (auto &replace : replaces_.list) {
1058 suggestionsSource_->stream() << "\
1059 { small(" << replace.id.size() << "), small(" << replace.replacement.size() << "), small(" << replace.words.size() << ") },\n";
1060 }
1061 suggestionsSource_->stream() << "};\n\
1062 \n\
1063 const medium ReplacementIndices[] = {";
1064 startBinary();
1065 for (auto &byCharIndex : byCharIndices) {
1066 for (auto index : byCharIndex) {
1067 writeIntBinary(suggestionsSource_.get(), index);
1068 }
1069 }
1070 suggestionsSource_->stream() << " };\n\
1071 \n\
1072 struct ReplacementIndexStruct {\n\
1073 utf16char ch;\n\
1074 medium count;\n\
1075 };\n\
1076 \n\
1077 const internal::checksum ReplacementChecksums[] = {\n";
1078 startBinary();
1079 for (auto &replace : replaces_.list) {
1080 writeUintBinary(suggestionsSource_.get(), countCrc32(replace.replacement.constData(), replace.replacement.size() * sizeof(QChar)));
1081 }
1082 suggestionsSource_->stream() << " };\n\
1083 \n\
1084 const ReplacementIndexStruct ReplacementIndexData[] = {\n";
1085 startBinary();
1086 for (auto i = byCharIndices.cbegin(), e = byCharIndices.cend(); i != e; ++i) {
1087 suggestionsSource_->stream() << "\
1088 { utf16char(" << i.key().unicode() << "), medium(" << i.value().size() << ") },\n";
1089 }
1090 suggestionsSource_->stream() << "};\n\
1091 \n\
1092 std::vector<Replacement> Replacements;\n\
1093 std::map<utf16char, std::vector<const Replacement*>> ReplacementsMap;\n\
1094 std::map<internal::checksum, const Replacement*> ReplacementsHash;\n\
1095 \n";
1096 return true;
1097 }
1098
writeGetReplacements()1099 bool Generator::writeGetReplacements() {
1100 suggestionsSource_->stream() << "\
1101 void InitReplacements() {\n\
1102 if (!Replacements.empty()) {\n\
1103 return;\n\
1104 }\n\
1105 auto data = ReplacementData;\n\
1106 auto takeString = [&data](int size) {\n\
1107 auto result = utf16string(data, size);\n\
1108 data += size;\n\
1109 return result;\n\
1110 };\n\
1111 auto wordSize = ReplacementWordLengths;\n\
1112 \n\
1113 Replacements.reserve(" << replaces_.list.size() << ");\n\
1114 for (auto item : ReplacementInitData) {\n\
1115 auto emoji = takeString(item.emojiSize);\n\
1116 auto replacement = takeString(item.replacementSize);\n\
1117 auto words = std::vector<utf16string>();\n\
1118 words.reserve(item.wordsCount);\n\
1119 for (auto i = 0; i != item.wordsCount; ++i) {\n\
1120 words.push_back(takeString(*wordSize++));\n\
1121 }\n\
1122 Replacements.push_back({ std::move(emoji), std::move(replacement), std::move(words) });\n\
1123 }\n\
1124 \n\
1125 auto indices = ReplacementIndices;\n\
1126 auto items = &Replacements[0];\n\
1127 for (auto item : ReplacementIndexData) {\n\
1128 auto index = std::vector<const Replacement*>();\n\
1129 index.reserve(item.count);\n\
1130 for (auto i = 0; i != item.count; ++i) {\n\
1131 index.push_back(items + (*indices++));\n\
1132 }\n\
1133 ReplacementsMap.emplace(item.ch, std::move(index));\n\
1134 }\n\
1135 \n\
1136 for (auto checksum : ReplacementChecksums) {\n\
1137 ReplacementsHash.emplace(checksum, items++);\n\
1138 }\n\
1139 }\n\
1140 \n\
1141 const std::vector<const Replacement*> *GetReplacements(utf16char first) {\n\
1142 if (ReplacementsMap.empty()) {\n\
1143 InitReplacements();\n\
1144 }\n\
1145 auto it = ReplacementsMap.find(first);\n\
1146 return (it == ReplacementsMap.cend()) ? nullptr : &it->second;\n\
1147 }\n\
1148 \n\
1149 const std::vector<Replacement> &GetAllReplacements() {\n\
1150 return Replacements;\n\
1151 }\n\
1152 \n\
1153 utf16string GetReplacementEmoji(utf16string replacement) {\n\
1154 auto code = internal::countChecksum(replacement.data(), replacement.size() * sizeof(utf16char));\n\
1155 auto it = ReplacementsHash.find(code);\n\
1156 return (it == ReplacementsHash.cend()) ? utf16string() : it->second->emoji;\n\
1157 }\n\
1158 \n";
1159 return true;
1160 }
1161
startBinary()1162 void Generator::startBinary() {
1163 _binaryFullLength = _binaryCount = 0;
1164 }
1165
writeStringBinary(common::CppFile * source,const QString & string)1166 bool Generator::writeStringBinary(common::CppFile *source, const QString &string) {
1167 if (string.size() >= 256) {
1168 logDataError() << "Too long string: " << string.toStdString();
1169 return false;
1170 }
1171 for (auto ch : string) {
1172 if (_binaryFullLength > 0) source->stream() << ",";
1173 if (!_binaryCount++) {
1174 source->stream() << "\n";
1175 } else {
1176 if (_binaryCount == 12) {
1177 _binaryCount = 0;
1178 }
1179 source->stream() << " ";
1180 }
1181 source->stream() << "0x" << QString::number(ch.unicode(), 16);
1182 ++_binaryFullLength;
1183 }
1184 return true;
1185 }
1186
writeIntBinary(common::CppFile * source,int data)1187 void Generator::writeIntBinary(common::CppFile *source, int data) {
1188 if (_binaryFullLength > 0) source->stream() << ",";
1189 if (!_binaryCount++) {
1190 source->stream() << "\n";
1191 } else {
1192 if (_binaryCount == 12) {
1193 _binaryCount = 0;
1194 }
1195 source->stream() << " ";
1196 }
1197 source->stream() << data;
1198 ++_binaryFullLength;
1199 }
1200
writeUintBinary(common::CppFile * source,uint32 data)1201 void Generator::writeUintBinary(common::CppFile *source, uint32 data) {
1202 if (_binaryFullLength > 0) source->stream() << ",";
1203 if (!_binaryCount++) {
1204 source->stream() << "\n";
1205 } else {
1206 if (_binaryCount == 12) {
1207 _binaryCount = 0;
1208 }
1209 source->stream() << " ";
1210 }
1211 source->stream() << "0x" << QString::number(data, 16).toUpper() << "U";
1212 ++_binaryFullLength;
1213 }
1214
1215 } // namespace emoji
1216 } // namespace codegen
1217