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 "emoji_config.h"
8 
9 #include "emoji_suggestions_helper.h"
10 #include "base/bytes.h"
11 #include "base/openssl_help.h"
12 #include "base/parse_helper.h"
13 #include "base/debug_log.h"
14 #include "ui/style/style_core.h"
15 #include "ui/painter.h"
16 #include "ui/ui_utility.h"
17 #include "styles/style_basic.h"
18 
19 #include <QtCore/QJsonDocument>
20 #include <QtCore/QJsonObject>
21 #include <QtCore/QFile>
22 #include <QtCore/QDir>
23 
24 #include <crl/crl_async.h>
25 
26 namespace Ui {
27 namespace Emoji {
28 namespace {
29 
30 constexpr auto kUniversalSize = 72;
31 constexpr auto kImagesPerRow = 32;
32 constexpr auto kImageRowsPerSprite = 16;
33 
34 constexpr auto kSetVersion = uint32(3);
35 constexpr auto kCacheVersion = uint32(7);
36 constexpr auto kMaxId = uint32(1 << 8);
37 
38 #ifdef Q_OS_MAC
39 constexpr auto kScaleForTouchBar = 150;
40 #endif
41 
42 enum class ConfigResult {
43 	Invalid,
44 	BadVersion,
45 	Good,
46 };
47 
48 // Right now we can't allow users of Ui::Emoji to create custom sizes.
49 // Any Instance::Instance() can invalidate Universal.id() and sprites.
50 // So all Instance::Instance() should happen before async generations.
51 class Instance {
52 public:
53 	explicit Instance(int size);
54 
55 	bool cached() const;
56 	void draw(QPainter &p, EmojiPtr emoji, int x, int y);
57 
58 private:
59 	void readCache();
60 	void generateCache();
61 	void checkUniversalImages();
62 	void pushSprite(QImage &&data);
63 
64 	int _id = 0;
65 	int _size = 0;
66 	std::vector<QPixmap> _sprites;
67 	base::binary_guard _generating;
68 	bool _unsupported = false;
69 
70 };
71 
72 auto SizeNormal = -1;
73 auto SizeLarge = -1;
74 auto SpritesCount = -1;
75 
76 auto InstanceNormal = std::unique_ptr<Instance>();
77 auto InstanceLarge = std::unique_ptr<Instance>();
78 auto Universal = std::shared_ptr<UniversalImages>();
79 auto CanClearUniversal = false;
80 auto WaitingToSwitchBackToId = 0;
81 auto Updates = rpl::event_stream<>();
82 
83 #ifdef Q_OS_MAC
84 auto TouchbarSize = -1;
85 auto TouchbarInstance = std::unique_ptr<Instance>();
86 auto TouchbarEmoji = (Instance*)nullptr;
87 #endif
88 
89 auto MainEmojiMap = std::map<int, QPixmap>();
90 auto OtherEmojiMap = base::flat_map<int, std::map<int, QPixmap>>();
91 
RowsCount(int index)92 int RowsCount(int index) {
93 	if (index + 1 < SpritesCount) {
94 		return kImageRowsPerSprite;
95 	}
96 	const auto count = internal::FullCount()
97 		- (index * kImagesPerRow * kImageRowsPerSprite);
98 	return (count / kImagesPerRow)
99 		+ ((count % kImagesPerRow) ? 1 : 0);
100 }
101 
CacheFileNameMask(int size)102 QString CacheFileNameMask(int size) {
103 	return "cache_" + QString::number(size) + '_';
104 }
105 
CacheFilePath(int size,int index)106 QString CacheFilePath(int size, int index) {
107 	return internal::CacheFileFolder()
108 		+ '/'
109 		+ CacheFileNameMask(size)
110 		+ QString::number(index);
111 }
112 
CurrentSettingPath()113 QString CurrentSettingPath() {
114 	return internal::CacheFileFolder() + "/current";
115 }
116 
IsValidSetId(int id)117 bool IsValidSetId(int id) {
118 	return (id == 0) || (id > 0 && id < kMaxId);
119 }
120 
ComputeVersion(int id)121 uint32 ComputeVersion(int id) {
122 	Expects(IsValidSetId(id));
123 
124 	static_assert(kCacheVersion > 0 && kCacheVersion < (1 << 16));
125 	static_assert(kSetVersion > 0 && kSetVersion < (1 << 8));
126 
127 	auto result = uint32(kCacheVersion);
128 	if (!id) {
129 		return result;
130 	}
131 	result |= (uint32(id) << 24) | (uint32(kSetVersion) << 16);
132 	return result;
133 }
134 
ReadCurrentSetId()135 int ReadCurrentSetId() {
136 	const auto path = CurrentSettingPath();
137 	auto file = QFile(path);
138 	if (!file.open(QIODevice::ReadOnly)) {
139 		return 0;
140 	}
141 	auto stream = QDataStream(&file);
142 	stream.setVersion(QDataStream::Qt_5_1);
143 	auto id = qint32(0);
144 	stream >> id;
145 	return (stream.status() == QDataStream::Ok && IsValidSetId(id))
146 		? id
147 		: 0;
148 }
149 
ApplyUniversalImages(std::shared_ptr<UniversalImages> images)150 void ApplyUniversalImages(std::shared_ptr<UniversalImages> images) {
151 	Universal = std::move(images);
152 	CanClearUniversal = false;
153 	MainEmojiMap.clear();
154 	OtherEmojiMap.clear();
155 	Updates.fire({});
156 }
157 
SwitchToSetPrepared(int id,std::shared_ptr<UniversalImages> images)158 void SwitchToSetPrepared(int id, std::shared_ptr<UniversalImages> images) {
159 	WaitingToSwitchBackToId = 0;
160 
161 	auto setting = QFile(CurrentSettingPath());
162 	if (!id) {
163 		setting.remove();
164 	} else if (setting.open(QIODevice::WriteOnly)) {
165 		auto stream = QDataStream(&setting);
166 		stream.setVersion(QDataStream::Qt_5_1);
167 		stream << qint32(id);
168 	}
169 	ApplyUniversalImages(std::move(images));
170 }
171 
ValidateConfig(int id)172 [[nodiscard]] ConfigResult ValidateConfig(int id) {
173 	Expects(IsValidSetId(id));
174 
175 	if (!id) {
176 		return ConfigResult::Good;
177 	}
178 	constexpr auto kSizeLimit = 65536;
179 	auto config = QFile(internal::SetDataPath(id) + "/config.json");
180 	if (!config.open(QIODevice::ReadOnly) || config.size() > kSizeLimit) {
181 		return ConfigResult::Invalid;
182 	}
183 	auto error = QJsonParseError{ 0, QJsonParseError::NoError };
184 	const auto document = QJsonDocument::fromJson(
185 		base::parse::stripComments(config.readAll()),
186 		&error);
187 	config.close();
188 	if (error.error != QJsonParseError::NoError) {
189 		return ConfigResult::Invalid;
190 	}
191 	if (document.object()["id"].toInt() != id) {
192 		return ConfigResult::Invalid;
193 	} else if (document.object()["version"].toInt() != kSetVersion) {
194 		return ConfigResult::BadVersion;
195 	}
196 	return ConfigResult::Good;
197 }
198 
ClearCurrentSetIdSync()199 void ClearCurrentSetIdSync() {
200 	Expects(Universal != nullptr);
201 
202 	const auto id = Universal->id();
203 	if (!id) {
204 		return;
205 	}
206 
207 	const auto newId = 0;
208 	auto universal = std::make_shared<UniversalImages>(newId);
209 	universal->ensureLoaded();
210 
211 	// Start loading the set when possible.
212 	ApplyUniversalImages(std::move(universal));
213 	WaitingToSwitchBackToId = id;
214 }
215 
SaveToFile(int id,const QImage & image,int size,int index)216 void SaveToFile(int id, const QImage &image, int size, int index) {
217 	Expects(image.bytesPerLine() == image.width() * 4);
218 
219 	QFile f(CacheFilePath(size, index));
220 	if (!f.open(QIODevice::WriteOnly)) {
221 		if (!QDir::current().mkpath(internal::CacheFileFolder())
222 			|| !f.open(QIODevice::WriteOnly)) {
223 			LOG(("App Error: Could not open emoji cache '%1' for size %2_%3"
224 				).arg(f.fileName()
225 				).arg(size
226 				).arg(index));
227 			return;
228 		}
229 	}
230 	const auto write = [&](bytes::const_span data) {
231 		return f.write(
232 			reinterpret_cast<const char*>(data.data()),
233 			data.size()
234 		) == data.size();
235 	};
236 	const uint32 header[] = {
237 		uint32(ComputeVersion(id)),
238 		uint32(size),
239 		uint32(image.width()),
240 		uint32(image.height()),
241 	};
242 	const auto data = bytes::const_span(
243 		reinterpret_cast<const bytes::type*>(image.bits()),
244 		image.width() * image.height() * 4);
245 	if (!write(bytes::make_span(header))
246 		|| !write(data)
247 		|| !write(openssl::Sha256(bytes::make_span(header), data))
248 		|| false) {
249 		LOG(("App Error: Could not write emoji cache '%1' for size %2"
250 			).arg(f.fileName()
251 			).arg(size));
252 	}
253 }
254 
LoadFromFile(int id,int size,int index)255 QImage LoadFromFile(int id, int size, int index) {
256 	const auto rows = RowsCount(index);
257 	const auto width = kImagesPerRow * size;
258 	const auto height = rows * size;
259 	const auto fileSize = 4 * sizeof(uint32)
260 		+ (width * height * 4)
261 		+ openssl::kSha256Size;
262 	QFile f(CacheFilePath(size, index));
263 	if (!f.exists()
264 		|| f.size() != fileSize
265 		|| !f.open(QIODevice::ReadOnly)) {
266 		return QImage();
267 	}
268 	const auto read = [&](bytes::span data) {
269 		return f.read(
270 			reinterpret_cast<char*>(data.data()),
271 			data.size()
272 		) == data.size();
273 	};
274 	uint32 header[4] = { 0 };
275 	if (!read(bytes::make_span(header))
276 		|| header[0] != ComputeVersion(id)
277 		|| header[1] != size
278 		|| header[2] != width
279 		|| header[3] != height) {
280 		return QImage();
281 	}
282 	auto result = QImage(
283 		width,
284 		height,
285 		QImage::Format_ARGB32_Premultiplied);
286 	Assert(result.bytesPerLine() == width * 4);
287 	const auto data = bytes::make_span(
288 		reinterpret_cast<bytes::type*>(result.bits()),
289 		width * height * 4);
290 	auto signature = bytes::vector(openssl::kSha256Size);
291 	if (!read(data)
292 		|| !read(signature)
293 		//|| (bytes::compare(
294 		//	signature,
295 		//	openssl::Sha256(bytes::make_span(header), data)) != 0)
296 		|| false) {
297 		return QImage();
298 	}
299 
300 	// This should remove a non necessary detach on Retina screens later.
301 	result.setDevicePixelRatio(style::DevicePixelRatio());
302 
303 	crl::async([=, signature = std::move(signature)] {
304 		// This should not happen (invalid signature),
305 		// so we delay this check and fix only the next launch.
306 		const auto data = bytes::make_span(
307 			reinterpret_cast<const bytes::type*>(result.bits()),
308 			width * height * 4);
309 		const auto result = bytes::compare(
310 			signature,
311 			openssl::Sha256(bytes::make_span(header), data));
312 		if (result != 0) {
313 			QFile(CacheFilePath(size, index)).remove();
314 		}
315 	});
316 	return result;
317 }
318 
LoadSprites(int id)319 std::vector<QImage> LoadSprites(int id) {
320 	Expects(IsValidSetId(id));
321 	Expects(SpritesCount > 0);
322 
323 	auto result = std::vector<QImage>();
324 	const auto folder = (id != 0)
325 		? internal::SetDataPath(id) + '/'
326 		: QStringLiteral(":/gui/emoji/");
327 	const auto base = folder + "emoji_";
328 	return ranges::views::ints(
329 		0,
330 		SpritesCount
331 	) | ranges::views::transform([&](int index) {
332 		return base + QString::number(index + 1) + ".webp";
333 	}) | ranges::views::transform([](const QString &path) {
334 		return QImage(path, "WEBP").convertToFormat(
335 			QImage::Format_ARGB32_Premultiplied);
336 	}) | ranges::to_vector;
337 }
338 
LoadAndValidateSprites(int id)339 std::vector<QImage> LoadAndValidateSprites(int id) {
340 	Expects(IsValidSetId(id));
341 	Expects(SpritesCount > 0);
342 
343 	const auto config = ValidateConfig(id);
344 	if (config != ConfigResult::Good) {
345 		return {};
346 	}
347 	auto result = LoadSprites(id);
348 	const auto sizes = ranges::views::ints(
349 		0,
350 		SpritesCount
351 	) | ranges::views::transform([](int index) {
352 		return QSize(
353 			kImagesPerRow * kUniversalSize,
354 			RowsCount(index) * kUniversalSize);
355 	});
356 	const auto good = ranges::views::zip_with(
357 		[](const QImage &data, QSize size) { return data.size() == size; },
358 		result,
359 		sizes);
360 	if (ranges::find(good, false) != end(good)) {
361 		return {};
362 	}
363 	return result;
364 }
365 
ClearUniversalChecked()366 void ClearUniversalChecked() {
367 	Expects(InstanceNormal != nullptr && InstanceLarge != nullptr);
368 
369 	if (CanClearUniversal
370 		&& Universal
371 		&& InstanceNormal->cached()
372 		&& InstanceLarge->cached()) {
373 		Universal->clear();
374 	}
375 }
376 
377 } // namespace
378 
379 namespace internal {
380 
CacheFileFolder()381 QString CacheFileFolder() {
382 	return Integration::Instance().emojiCacheFolder();
383 }
384 
SetDataPath(int id)385 QString SetDataPath(int id) {
386 	Expects(IsValidSetId(id) && id != 0);
387 
388 	return CacheFileFolder() + "/set" + QString::number(id);
389 }
390 
391 } // namespace internal
392 
UniversalImages(int id)393 UniversalImages::UniversalImages(int id) : _id(id) {
394 	Expects(IsValidSetId(id));
395 }
396 
id() const397 int UniversalImages::id() const {
398 	return _id;
399 }
400 
ensureLoaded()401 bool UniversalImages::ensureLoaded() {
402 	Expects(SpritesCount > 0);
403 
404 	if (!_sprites.empty()) {
405 		return true;
406 	}
407 	_sprites = LoadAndValidateSprites(_id);
408 	return !_sprites.empty();
409 }
410 
clear()411 void UniversalImages::clear() {
412 	_sprites.clear();
413 }
414 
draw(QPainter & p,EmojiPtr emoji,int size,int x,int y) const415 void UniversalImages::draw(
416 		QPainter &p,
417 		EmojiPtr emoji,
418 		int size,
419 		int x,
420 		int y) const {
421 	Expects(emoji->sprite() < _sprites.size());
422 
423 	const auto large = kUniversalSize;
424 	const auto &original = _sprites[emoji->sprite()];
425 	const auto data = original.bits();
426 	const auto stride = original.bytesPerLine();
427 	const auto format = original.format();
428 	const auto row = emoji->row();
429 	const auto column = emoji->column();
430 	auto single = QImage(
431 		data + (row * kImagesPerRow * large + column) * large * 4,
432 		large,
433 		large,
434 		stride,
435 		format
436 	).scaled(
437 		size,
438 		size,
439 		Qt::IgnoreAspectRatio,
440 		Qt::SmoothTransformation);
441 	single.setDevicePixelRatio(p.device()->devicePixelRatio());
442 	p.drawImage(x, y, single);
443 }
444 
generate(int size,int index) const445 QImage UniversalImages::generate(int size, int index) const {
446 	Expects(size > 0);
447 	Expects(index < _sprites.size());
448 
449 	const auto rows = RowsCount(index);
450 	const auto large = kUniversalSize;
451 	const auto &original = _sprites[index];
452 	const auto data = original.bits();
453 	const auto stride = original.bytesPerLine();
454 	const auto format = original.format();
455 	auto result = QImage(
456 		size * kImagesPerRow,
457 		size * rows,
458 		QImage::Format_ARGB32_Premultiplied);
459 	result.fill(Qt::transparent);
460 	{
461 		QPainter p(&result);
462 		for (auto y = 0; y != rows; ++y) {
463 			for (auto x = 0; x != kImagesPerRow; ++x) {
464 				const auto single = QImage(
465 					data + (y * kImagesPerRow * large + x) * large * 4,
466 					large,
467 					large,
468 					stride,
469 					format
470 				).scaled(
471 					size,
472 					size,
473 					Qt::IgnoreAspectRatio,
474 					Qt::SmoothTransformation);
475 				p.drawImage(
476 					x * size,
477 					y * size,
478 					single);
479 			}
480 		}
481 	}
482 	SaveToFile(_id, result, size, index);
483 	return result;
484 }
485 
Init()486 void Init() {
487 	internal::Init();
488 
489 	const auto count = internal::FullCount();
490 	const auto persprite = kImagesPerRow * kImageRowsPerSprite;
491 	SpritesCount = (count / persprite) + ((count % persprite) ? 1 : 0);
492 
493 	SizeNormal = style::ConvertScale(18, style::Scale() * style::DevicePixelRatio());
494 	SizeLarge = int(style::ConvertScale(18 * 4 / 3., style::Scale() * style::DevicePixelRatio()));
495 	Universal = std::make_shared<UniversalImages>(ReadCurrentSetId());
496 	CanClearUniversal = false;
497 
498 	InstanceNormal = std::make_unique<Instance>(SizeNormal);
499 	InstanceLarge = std::make_unique<Instance>(SizeLarge);
500 
501 #ifdef Q_OS_MAC
502 	if (style::Scale() != kScaleForTouchBar) {
503 		TouchbarSize = int(style::ConvertScale(18 * 4 / 3.,
504 			kScaleForTouchBar * style::DevicePixelRatio()));
505 		TouchbarInstance = std::make_unique<Instance>(TouchbarSize);
506 		TouchbarEmoji = TouchbarInstance.get();
507 	} else {
508 		TouchbarEmoji = InstanceLarge.get();
509 	}
510 #endif
511 }
512 
Clear()513 void Clear() {
514 	MainEmojiMap.clear();
515 	OtherEmojiMap.clear();
516 
517 	InstanceNormal = nullptr;
518 	InstanceLarge = nullptr;
519 #ifdef Q_OS_MAC
520 	TouchbarInstance = nullptr;
521 	TouchbarEmoji = nullptr;
522 #endif
523 }
524 
ClearIrrelevantCache()525 void ClearIrrelevantCache() {
526 	Expects(SizeNormal > 0);
527 	Expects(SizeLarge > 0);
528 
529 	crl::async([] {
530 		const auto folder = internal::CacheFileFolder();
531 		const auto list = QDir(folder).entryList(QDir::Files);
532 		const auto good1 = CacheFileNameMask(SizeNormal);
533 		const auto good2 = CacheFileNameMask(SizeLarge);
534 		const auto good3full = CurrentSettingPath();
535 		for (const auto &name : list) {
536 			if (!name.startsWith(good1) && !name.startsWith(good2)) {
537 				const auto full = folder + '/' + name;
538 				if (full != good3full) {
539 					QFile(full).remove();
540 				}
541 			}
542 		}
543 	});
544 }
545 
CurrentSetId()546 int CurrentSetId() {
547 	Expects(Universal != nullptr);
548 
549 	return Universal->id();
550 }
551 
NeedToSwitchBackToId()552 int NeedToSwitchBackToId() {
553 	return WaitingToSwitchBackToId;
554 }
555 
ClearNeedSwitchToId()556 void ClearNeedSwitchToId() {
557 	if (!WaitingToSwitchBackToId) {
558 		return;
559 	}
560 	WaitingToSwitchBackToId = 0;
561 	QFile(CurrentSettingPath()).remove();
562 }
563 
SwitchToSet(int id,Fn<void (bool)> callback)564 void SwitchToSet(int id, Fn<void(bool)> callback) {
565 	Expects(IsValidSetId(id));
566 
567 	if (Universal && Universal->id() == id) {
568 		callback(true);
569 		return;
570 	}
571 	crl::async([=] {
572 		auto universal = std::make_shared<UniversalImages>(id);
573 		if (!universal->ensureLoaded()) {
574 			crl::on_main([=] {
575 				callback(false);
576 			});
577 		} else {
578 			crl::on_main([=, universal = std::move(universal)]() mutable {
579 				SwitchToSetPrepared(id, std::move(universal));
580 				callback(true);
581 			});
582 		}
583 	});
584 }
585 
SetIsReady(int id)586 bool SetIsReady(int id) {
587 	Expects(IsValidSetId(id));
588 
589 	if (!id) {
590 		return true;
591 	}
592 	const auto folder = internal::SetDataPath(id) + '/';
593 	auto names = ranges::views::ints(
594 		0,
595 		SpritesCount + 1
596 	) | ranges::views::transform([](int index) {
597 		return index
598 			? "emoji_" + QString::number(index) + ".webp"
599 			: QString("config.json");
600 	});
601 	const auto bad = ranges::find_if(names, [&](const QString &name) {
602 		return !QFile(folder + name).exists();
603 	});
604 	return (bad == names.end());
605 }
606 
Updated()607 rpl::producer<> Updated() {
608 	return Updates.events();
609 }
610 
GetSizeNormal()611 int GetSizeNormal() {
612 	Expects(SizeNormal > 0);
613 
614 	return SizeNormal;
615 }
616 
GetSizeLarge()617 int GetSizeLarge() {
618 	Expects(SizeLarge > 0);
619 
620 	return SizeLarge;
621 }
622 
623 #ifdef Q_OS_MAC
GetSizeTouchbar()624 int GetSizeTouchbar() {
625 	return (style::Scale() == kScaleForTouchBar)
626 		? GetSizeLarge()
627 		: TouchbarSize;
628 }
629 #endif
630 
variantsCount() const631 int One::variantsCount() const {
632 	return hasVariants() ? 5 : 0;
633 }
634 
variantIndex(EmojiPtr variant) const635 int One::variantIndex(EmojiPtr variant) const {
636 	return (variant - original());
637 }
638 
variant(int index) const639 EmojiPtr One::variant(int index) const {
640 	return (index >= 0 && index <= variantsCount()) ? (original() + index) : this;
641 }
642 
IdFromOldKey(uint64 oldKey)643 QString IdFromOldKey(uint64 oldKey) {
644 	auto code = uint32(oldKey >> 32);
645 	auto code2 = uint32(oldKey & 0xFFFFFFFFLLU);
646 	if (!code && code2) {
647 		code = base::take(code2);
648 	}
649 	if ((code & 0xFFFF0000U) != 0xFFFF0000U) { // code and code2 contain the whole id
650 		auto result = QString();
651 		result.reserve(4);
652 		auto addCode = [&result](uint32 code) {
653 			if (auto high = (code >> 16)) {
654 				result.append(QChar(static_cast<ushort>(high & 0xFFFFU)));
655 			}
656 			result.append(QChar(static_cast<ushort>(code & 0xFFFFU)));
657 		};
658 		addCode(code);
659 		if (code2) addCode(code2);
660 		return result;
661 	}
662 
663 	// old sequence
664 	auto sequenceIndex = int(code & 0xFFFFU);
665 	switch (sequenceIndex) {
666 	case 0: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7");
667 	case 1: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6");
668 	case 2: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6");
669 	case 3: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7");
670 	case 4: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6");
671 	case 5: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7");
672 	case 6: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6");
673 	case 7: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6");
674 	case 8: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7");
675 	case 9: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6");
676 	case 10: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7");
677 	case 11: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6");
678 	case 12: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6");
679 	case 13: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7");
680 	case 14: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9");
681 	case 15: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8");
682 	case 16: return QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9");
683 	case 17: return QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8");
684 	case 18: return QString::fromUtf8("\xf0\x9f\x91\x81\xe2\x80\x8d\xf0\x9f\x97\xa8");
685 	}
686 	return QString();
687 }
688 
GetDefaultRecent()689 QVector<EmojiPtr> GetDefaultRecent() {
690 	const auto defaultRecent = {
691 		0xD83DDE02LLU,
692 		0xD83DDE18LLU,
693 		0x2764LLU,
694 		0xD83DDE0DLLU,
695 		0xD83DDE0ALLU,
696 		0xD83DDE01LLU,
697 		0xD83DDC4DLLU,
698 		0x263ALLU,
699 		0xD83DDE14LLU,
700 		0xD83DDE04LLU,
701 		0xD83DDE2DLLU,
702 		0xD83DDC8BLLU,
703 		0xD83DDE12LLU,
704 		0xD83DDE33LLU,
705 		0xD83DDE1CLLU,
706 		0xD83DDE48LLU,
707 		0xD83DDE09LLU,
708 		0xD83DDE03LLU,
709 		0xD83DDE22LLU,
710 		0xD83DDE1DLLU,
711 		0xD83DDE31LLU,
712 		0xD83DDE21LLU,
713 		0xD83DDE0FLLU,
714 		0xD83DDE1ELLU,
715 		0xD83DDE05LLU,
716 		0xD83DDE1ALLU,
717 		0xD83DDE4ALLU,
718 		0xD83DDE0CLLU,
719 		0xD83DDE00LLU,
720 		0xD83DDE0BLLU,
721 		0xD83DDE06LLU,
722 		0xD83DDC4CLLU,
723 		0xD83DDE10LLU,
724 		0xD83DDE15LLU,
725 	};
726 	auto result = QVector<EmojiPtr>();
727 	for (const auto oldKey : defaultRecent) {
728 		if (const auto emoji = FromOldKey(oldKey)) {
729 			result.push_back(emoji);
730 		}
731 	}
732 	return result;
733 }
734 
SinglePixmap(EmojiPtr emoji,int fontHeight)735 const QPixmap &SinglePixmap(EmojiPtr emoji, int fontHeight) {
736 	const auto factor = style::DevicePixelRatio();
737 	auto &map = (fontHeight == st::normalFont->height * factor)
738 		? MainEmojiMap
739 		: OtherEmojiMap[fontHeight];
740 	auto i = map.find(emoji->index());
741 	if (i != end(map)) {
742 		return i->second;
743 	}
744 	auto image = QImage(
745 		SizeNormal + st::emojiPadding * factor * 2,
746 		fontHeight,
747 		QImage::Format_ARGB32_Premultiplied);
748 	image.setDevicePixelRatio(factor);
749 	image.fill(Qt::transparent);
750 	{
751 		QPainter p(&image);
752 		PainterHighQualityEnabler hq(p);
753 		Draw(
754 			p,
755 			emoji,
756 			SizeNormal,
757 			st::emojiPadding,
758 			(fontHeight - SizeNormal) / (2 * factor));
759 	}
760 	return map.emplace(
761 		emoji->index(),
762 		PixmapFromImage(std::move(image))
763 	).first->second;
764 }
765 
Draw(QPainter & p,EmojiPtr emoji,int size,int x,int y)766 void Draw(QPainter &p, EmojiPtr emoji, int size, int x, int y) {
767 #ifdef Q_OS_MAC
768 	const auto s = (style::Scale() == kScaleForTouchBar)
769 		? SizeLarge
770 		: TouchbarSize;
771 	if (size == s) {
772 		TouchbarEmoji->draw(p, emoji, x, y);
773 		return;
774 	}
775 #endif
776 	if (size == SizeNormal) {
777 		InstanceNormal->draw(p, emoji, x, y);
778 	} else if (size == SizeLarge) {
779 		InstanceLarge->draw(p, emoji, x, y);
780 	} else {
781 		Unexpected("Size in Ui::Emoji::Draw.");
782 	}
783 }
784 
Instance(int size)785 Instance::Instance(int size) : _id(Universal->id()), _size(size) {
786 	Expects(Universal != nullptr);
787 
788 	readCache();
789 	if (!cached()) {
790 		generateCache();
791 	}
792 }
793 
cached() const794 bool Instance::cached() const {
795 	Expects(Universal != nullptr);
796 
797 	return (Universal->id() == _id) && (_sprites.size() == SpritesCount);
798 }
799 
draw(QPainter & p,EmojiPtr emoji,int x,int y)800 void Instance::draw(QPainter &p, EmojiPtr emoji, int x, int y) {
801 	if (_unsupported) {
802 		return;
803 	} else if (Universal && Universal->id() != _id) {
804 		generateCache();
805 	}
806 	const auto sprite = emoji->sprite();
807 	if (sprite >= _sprites.size()) {
808 		Assert(Universal != nullptr);
809 		Universal->draw(p, emoji, _size, x, y);
810 		return;
811 	}
812 	p.drawPixmap(
813 		QPoint(x, y),
814 		_sprites[sprite],
815 		QRect(emoji->column() * _size, emoji->row() * _size, _size, _size));
816 }
817 
readCache()818 void Instance::readCache() {
819 	for (auto i = 0; i != SpritesCount; ++i) {
820 		auto image = LoadFromFile(_id, _size, i);
821 		if (image.isNull()) {
822 			return;
823 		}
824 		pushSprite(std::move(image));
825 	}
826 }
827 
checkUniversalImages()828 void Instance::checkUniversalImages() {
829 	Expects(Universal != nullptr);
830 
831 	if (_id != Universal->id()) {
832 		_id = Universal->id();
833 		_generating = nullptr;
834 		_sprites.clear();
835 	}
836 	if (!Universal->ensureLoaded()) {
837 		if (Universal->id() != 0) {
838 			ClearCurrentSetIdSync();
839 		} else {
840 			_unsupported = true;
841 		}
842 	}
843 }
844 
generateCache()845 void Instance::generateCache() {
846 	checkUniversalImages();
847 
848 	const auto cachePath = internal::CacheFileFolder();
849 	if (cachePath.isEmpty()) {
850 		return;
851 	}
852 	const auto size = _size;
853 	const auto index = _sprites.size();
854 	crl::async([
855 		=,
856 		universal = Universal,
857 		guard = _generating.make_guard()
858 	]() mutable {
859 		auto image = universal->generate(size, index);
860 		crl::on_main(std::move(guard), [
861 			=,
862 			image = std::move(image)
863 		]() mutable {
864 			if (universal != Universal) {
865 				return;
866 			}
867 			pushSprite(std::move(image));
868 			if (cached()) {
869 				ClearUniversalChecked();
870 			} else {
871 				generateCache();
872 			}
873 		});
874 	});
875 }
876 
pushSprite(QImage && data)877 void Instance::pushSprite(QImage &&data) {
878 	_sprites.push_back(PixmapFromImage(std::move(data)));
879 	_sprites.back().setDevicePixelRatio(style::DevicePixelRatio());
880 }
881 
SourceImages()882 const std::shared_ptr<UniversalImages> &SourceImages() {
883 	return Universal;
884 }
885 
ClearSourceImages(const std::shared_ptr<UniversalImages> & images)886 void ClearSourceImages(const std::shared_ptr<UniversalImages> &images) {
887 	if (images == Universal) {
888 		CanClearUniversal = true;
889 		ClearUniversalChecked();
890 	}
891 }
892 
ReplaceSourceImages(std::shared_ptr<UniversalImages> images)893 void ReplaceSourceImages(std::shared_ptr<UniversalImages> images) {
894 	Expects(images != nullptr);
895 
896 	if (Universal->id() == images->id()) {
897 		Universal = std::move(images);
898 	}
899 }
900 
901 } // namespace Emoji
902 } // namespace Ui
903