/src/serenity/Userland/Libraries/LibGfx/Font/Emoji.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org> |
3 | | * Copyright (c) 2022, Linus Groh <linusg@serenityos.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <AK/Debug.h> |
9 | | #include <AK/HashMap.h> |
10 | | #include <AK/LexicalPath.h> |
11 | | #include <AK/Span.h> |
12 | | #include <AK/Utf32View.h> |
13 | | #include <AK/Utf8View.h> |
14 | | #include <AK/Variant.h> |
15 | | #include <LibGfx/Bitmap.h> |
16 | | #include <LibGfx/Font/Emoji.h> |
17 | | #include <LibUnicode/CharacterTypes.h> |
18 | | #include <LibUnicode/Emoji.h> |
19 | | |
20 | | namespace Gfx { |
21 | | |
22 | | // https://unicode.org/reports/tr51/ |
23 | | // https://unicode.org/emoji/charts/emoji-list.html |
24 | | // https://unicode.org/emoji/charts/emoji-zwj-sequences.html |
25 | | |
26 | | static HashMap<StringView, RefPtr<Gfx::Bitmap>> s_emojis; |
27 | | static Variant<String, StringView> s_emoji_lookup_path = "/res/emoji"sv; |
28 | | |
29 | | static StringView emoji_lookup_path() |
30 | 0 | { |
31 | 0 | return s_emoji_lookup_path.visit([](auto const& path) -> StringView { return path; });Unexecuted instantiation: Emoji.cpp:AK::StringView Gfx::emoji_lookup_path()::$_0::operator()<AK::String>(AK::String const&) const Unexecuted instantiation: Emoji.cpp:AK::StringView Gfx::emoji_lookup_path()::$_0::operator()<AK::StringView>(AK::StringView const&) const |
32 | 0 | } |
33 | | |
34 | | void Emoji::set_emoji_lookup_path(String emoji_lookup_path) |
35 | 0 | { |
36 | 0 | s_emoji_lookup_path = move(emoji_lookup_path); |
37 | 0 | } |
38 | | |
39 | | Bitmap const* Emoji::emoji_for_code_point(u32 code_point) |
40 | 0 | { |
41 | 0 | return emoji_for_code_points(Array { code_point }); |
42 | 0 | } |
43 | | |
44 | | Bitmap const* Emoji::emoji_for_code_points(ReadonlySpan<u32> const& code_points) |
45 | 0 | { |
46 | 0 | auto emoji = Unicode::find_emoji_for_code_points(code_points); |
47 | 0 | if (!emoji.has_value() || !emoji->image_path.has_value()) |
48 | 0 | return nullptr; |
49 | | |
50 | 0 | auto emoji_file = emoji->image_path.value(); |
51 | 0 | if (auto it = s_emojis.find(emoji_file); it != s_emojis.end()) |
52 | 0 | return it->value.ptr(); |
53 | | |
54 | 0 | auto emoji_path = LexicalPath::join(emoji_lookup_path(), emoji_file); |
55 | 0 | auto bitmap_or_error = Bitmap::load_from_file(emoji_path.string()); |
56 | |
|
57 | 0 | if (bitmap_or_error.is_error()) { |
58 | 0 | dbgln_if(EMOJI_DEBUG, "Generated emoji data has file {}, but could not load image: {}", emoji_file, bitmap_or_error.error()); |
59 | 0 | s_emojis.set(emoji_file, nullptr); |
60 | 0 | return nullptr; |
61 | 0 | } |
62 | | |
63 | 0 | auto bitmap = bitmap_or_error.release_value(); |
64 | 0 | s_emojis.set(emoji_file, bitmap); |
65 | 0 | return bitmap.ptr(); |
66 | 0 | } |
67 | | |
68 | | template<typename CodePointIterator> |
69 | | static Bitmap const* emoji_for_code_point_iterator_impl(CodePointIterator& it) |
70 | 0 | { |
71 | 0 | if (!Unicode::could_be_start_of_emoji_sequence(it)) |
72 | 0 | return nullptr; |
73 | | |
74 | 0 | constexpr size_t max_emoji_code_point_sequence_length = 10; |
75 | |
|
76 | 0 | Vector<u32, max_emoji_code_point_sequence_length> code_points; |
77 | |
|
78 | 0 | struct EmojiAndCodePoints { |
79 | 0 | Bitmap const* emoji; |
80 | 0 | Span<u32> code_points; |
81 | 0 | u8 real_codepoint_length; |
82 | 0 | }; |
83 | 0 | Vector<EmojiAndCodePoints, max_emoji_code_point_sequence_length> possible_emojis; |
84 | | |
85 | | // Determine all existing emojis for the longest possible ZWJ emoji sequence, |
86 | | // or until we run out of code points in the iterator. |
87 | 0 | bool last_codepoint_sequence_found = false; |
88 | 0 | for (u8 i = 0; i < max_emoji_code_point_sequence_length; ++i) { |
89 | 0 | auto code_point = it.peek(i); |
90 | 0 | if (!code_point.has_value()) |
91 | 0 | break; |
92 | | // NOTE: The following only applies to emoji presentation, not to other |
93 | | // emoji modifiers. |
94 | | // |
95 | | // For a single emoji core sequence, we assume that emoji presentation |
96 | | // is implied, since this function will only be called for characters |
97 | | // with default text presentation when either (1) the character is not |
98 | | // found in the font, or (2) the character is followed by an explicit |
99 | | // emoji presentation selector. |
100 | | // |
101 | | // For emoji zwj sequences, Serenity chooses to treat minimally-qualified |
102 | | // and unqualified emojis the same as fully-qualified emojis (with regards |
103 | | // to emoji presentation). |
104 | | // |
105 | | // From https://unicode.org/reports/tr51/#Emoji_Implementation_Notes: |
106 | | // > minimally-qualified or unqualified emoji zwj sequences may be handled |
107 | | // > in the same way as their fully-qualified forms; the choice is up to |
108 | | // > the implementation. |
109 | | // |
110 | | // In both cases, whenever an emoji presentation selector (U+FE0F) is found, we |
111 | | // just skip it in order to drop fully-qualified emojis down to their |
112 | | // minimally-qualified or unqualified forms (with respect to emoji presentation) |
113 | | // for doing emoji lookups. This ensures that all forms are treated the same |
114 | | // assuming the emoji filenames are named accordingly (with all emoji presentation |
115 | | // selector codepoints removed). |
116 | 0 | if (code_point.value() == 0xFE0F) { |
117 | | // If the last sequence was found, then we can just update |
118 | | // its real length. |
119 | 0 | if (last_codepoint_sequence_found) { |
120 | 0 | possible_emojis.last().real_codepoint_length++; |
121 | 0 | } |
122 | | // And we can always skip the lookup since the code point sequence |
123 | | // will be unchanged since last time. |
124 | 0 | continue; |
125 | 0 | } else { |
126 | 0 | code_points.append(*code_point); |
127 | 0 | } |
128 | 0 | if (auto const* emoji = Emoji::emoji_for_code_points(code_points)) { |
129 | 0 | u8 real_codepoint_length = i + 1; |
130 | 0 | possible_emojis.empend(emoji, code_points, real_codepoint_length); |
131 | 0 | last_codepoint_sequence_found = true; |
132 | 0 | } else { |
133 | 0 | last_codepoint_sequence_found = false; |
134 | 0 | } |
135 | 0 | } |
136 | |
|
137 | 0 | if (possible_emojis.is_empty()) |
138 | 0 | return nullptr; |
139 | | |
140 | | // If we found one or more matches, return the longest, i.e. last. For example: |
141 | | // U+1F3F3 - white flag |
142 | | // U+1F3F3 U+200D U+1F308 - rainbow flag (unqualified form) |
143 | 0 | auto& [emoji, emoji_code_points, codepoint_length] = possible_emojis.last(); |
144 | | |
145 | | // Advance the iterator, so it's on the last code point of our found emoji and |
146 | | // whoever is iterating will advance to the next new code point. |
147 | 0 | for (u8 i = 0; i < codepoint_length - 1; ++i) |
148 | 0 | ++it; |
149 | |
|
150 | 0 | return emoji; |
151 | 0 | } Unexecuted instantiation: Emoji.cpp:Gfx::Bitmap const* Gfx::emoji_for_code_point_iterator_impl<AK::Utf8CodePointIterator>(AK::Utf8CodePointIterator&) Unexecuted instantiation: Emoji.cpp:Gfx::Bitmap const* Gfx::emoji_for_code_point_iterator_impl<AK::Utf32CodePointIterator>(AK::Utf32CodePointIterator&) |
152 | | |
153 | | Bitmap const* Emoji::emoji_for_code_point_iterator(Utf8CodePointIterator& it) |
154 | 0 | { |
155 | 0 | return emoji_for_code_point_iterator_impl(it); |
156 | 0 | } |
157 | | |
158 | | Bitmap const* Emoji::emoji_for_code_point_iterator(Utf32CodePointIterator& it) |
159 | 0 | { |
160 | 0 | return emoji_for_code_point_iterator_impl(it); |
161 | 0 | } |
162 | | |
163 | | } |