// SPDX-License-Identifier: GPL-3.0-or-later // Avatar text (#674, #785): the initials and the emoji icon taken from a name. // Build: g++ -std=c++17 -I src -o /tmp/test_avatar_text test/test_avatar_text.cpp #include #include #include #include #include "ui-touch/AvatarText.h" // A font like the device's: ASCII and Latin-1, no flags or emoji. static bool latinFont(uint32_t cp, void*) { return cp < 0x100; } // A narrower one: ASCII only, so an accented letter needs its plain letter. static bool asciiFont(uint32_t cp, void*) { return cp < 0x80; } // The firmware's chained faces, which also carry Greek and Cyrillic. static bool wideFont(uint32_t cp, void*) { return cp < 0x530; } // The baked emoji set as the firmware has it: the rows that matter here. The joiner // and the variation selectors are in the table as an empty 2 x 2 image (so text never // shows them); the visible check is what the UI passes, true for a real picture only. static const std::set kTable = {0x200D, 0xFE0E, 0xFE0F, 0x1F1F8, 0x1F600, 0x1F44D}; static const std::set kInvisible = {0x200D, 0xFE0E, 0xFE0F, 0x1F1F8}; static bool anyRow(uint32_t cp, void*) { return kTable.count(cp) != 0; } // the old check static bool visibleRow(uint32_t cp, void*) { return kTable.count(cp) && !kInvisible.count(cp); } static std::string ini(const char* name, AvatarText::HasGlyph has = latinFont) { char out[12]; AvatarText::initials(name, out, sizeof out, has, nullptr); return out; } // The names from the report: a flag (U+1F1F5 U+1F1F9) and an island with its emoji // variation selector (U+1F3DD U+FE0F), and a man (U+1F468) after the name. static const char kTristaoEdc[] = "\xF0\x9F\x87\xB5\xF0\x9F\x87\xB9\xF0\x9F\x8F\x9D\xEF\xB8\x8F Trist\xC3\xA3o EDC"; static const char kTristaoMobile[] = "Trist\xC3\xA3o Mobile \xF0\x9F\x91\xA8"; static const char kTristaoZwj[] = "Trist\xC3\xA3o \xF0\x9F\x91\xA8\xE2\x80\x8D\xF0\x9F\x92\xBB"; // man + ZWJ + laptop int main() { char eb[20]; // The bug, reproduced with the old rule (any table row counts): the island's // variation selector is the "first emoji", an empty image, so the tile is blank. assert(AvatarText::firstEmoji(kTristaoEdc, eb, sizeof eb, anyRow, nullptr)); assert(!strcmp(eb, "\xEF\xB8\x8F")); assert(AvatarText::firstEmoji(kTristaoZwj, eb, sizeof eb, anyRow, nullptr)); assert(!strcmp(eb, "\xE2\x80\x8D")); // Fixed: only a visible picture is an icon; these names keep their dot pattern. assert(!AvatarText::firstEmoji(kTristaoEdc, eb, sizeof eb, visibleRow, nullptr)); assert(!AvatarText::firstEmoji(kTristaoMobile, eb, sizeof eb, visibleRow, nullptr)); assert(!AvatarText::firstEmoji(kTristaoZwj, eb, sizeof eb, visibleRow, nullptr)); assert(!AvatarText::firstEmoji("\xF0\x9F\x87\xB8\xF0\x9F\x87\xAA Sweden", eb, sizeof eb, visibleRow, nullptr)); // A real emoji still becomes the icon (#640), after a joiner too. assert(AvatarText::firstEmoji("Base \xEF\xB8\x8F\xF0\x9F\x98\x80 camp", eb, sizeof eb, visibleRow, nullptr)); assert(!strcmp(eb, "\xF0\x9F\x98\x80")); assert(!AvatarText::firstEmoji("\xC2\xA9 corp", eb, sizeof eb, visibleRow, nullptr)); assert(!AvatarText::firstEmoji("Plain name", eb, sizeof eb, visibleRow, nullptr)); // Initials: whole code points, in capitals, emoji and symbols skipped. assert(ini("\xC3\xA3na") == "\xC3\x83N"); // "ãna" -> "ÃN" assert(ini("Jo\xC3\xA3o") == "JO"); assert(ini("Jo""a\xCC\x83o") == "JO"); // a + combining tilde assert(ini("\xC3\x89mile") == "\xC3\x89M"); // "Émile" -> "ÉM" assert(ini("Zo\xC3\xAB") == "ZO"); assert(ini(kTristaoEdc) == "TE"); // was the two flag halves (#674) assert(ini(kTristaoMobile) == "TM"); // the emoji word holds no letter assert(ini("#hiking") == "HI"); assert(ini(" [base] camp") == "BC"); assert(ini("42 Node") == "4N"); assert(ini("\xD0\xB4\xD0\xB8\xD0\xBC\xD0\xB0", wideFont) == "\xD0\x94\xD0\x98"); // Cyrillic "дима" -> "ДИ" // #785: the first letter of the first word and of the LAST word, not the first two. assert(ini("Eddie Smith") == "ES"); assert(ini("john paul jones") == "JJ"); assert(ini("Eddie-Base") == "EB"); assert(ini("kd2abc_mobile") == "KM"); assert(ini("Home/Roof Node") == "HN"); assert(ini(" Eddie Smith ") == "ES"); // runs of spaces, edges assert(ini("\xC3\xA9mile Zola") == "\xC3\x89Z"); // "émile Zola" -> "ÉZ" assert(ini("Joa\xCC\x83o Silva") == "JS"); // the combining tilde is no break assert(ini("Base \xF0\x9F\x91\x8D") == "BA"); // emoji-only word: one word left assert(ini("\xF0\x9F\x91\x8D Base Camp") == "BC"); assert(ini("[Eddie] (Smith)") == "ES"); // brackets are skipped, not words assert(ini("Eddie #2") == "E2"); assert(ini("\xD0\x94\xD0\xB8\xD0\xBC\xD0\xB0 \xD0\x9F\xD0\xB5\xD1\x82\xD1\x80\xD0\xBE\xD0\xB2", wideFont) == "\xD0\x94\xD0\x9F"); // "Дима Петров" -> "ДП" // A word whose letters the font cannot draw is passed over, like an emoji word. assert(ini("Eddie \xD0\x94\xD0\xB8\xD0\xBC\xD0\xB0", latinFont) == "ED"); // One word: the first two letters, as before. assert(ini("Eddie") == "ED"); assert(ini("E") == "E"); // A font without the accented capital: its plain letter. assert(ini("\xC3\xA3na", asciiFont) == "AN"); // Nothing to draw: "?", never blank. assert(ini("\xF0\x9F\x87\xB5\xF0\x9F\x87\xB9") == "?"); assert(ini("") == "?"); assert(ini(nullptr) == "?"); assert(ini("\xFF\xFE") == "?"); // malformed bytes // Two letters only, and never past the buffer. char small[4]; AvatarText::initials("\xC3\xA3\xC3\xA9", small, sizeof small, latinFont, nullptr); assert(!strcmp(small, "\xC3\x83")); // the second does not fit // The uppercase table the section labels use, moved here unchanged. assert(AvatarText::upper('a') == 'A' && AvatarText::upper(0xE3) == 0xC3 && AvatarText::upper(0xF7) == 0xF7); assert(AvatarText::upper(0x101) == 0x100 && AvatarText::upper(0x3C2) == 0x3A3 && AvatarText::upper(0x451) == 0x401); puts("avatar text: emoji icon skips joiners, initials are first + last word, never blank PASS"); }