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