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