#pragma once // Emoji sprites for e-ink display - dual size // Large (12x12) for compose/picker, Small (10x10) for channel view // MSB-first, 2 bytes per row #include #ifdef ESP32 #include #endif // Large sprites for compose screen and picker #define EMOJI_LG_W 12 #define EMOJI_LG_H 12 // Small sprites for channel message view #define EMOJI_SM_W 10 #define EMOJI_SM_H 10 #define EMOJI_COUNT 20 // Escape codes used in sanitized message text // Bytes 0x01 through 0x14 map to emoji indices 0-19 #define EMOJI_ESCAPE_START 0x01 #define EMOJI_ESCAPE_END 0x14 #define EMOJI_PAD_BYTE 0x15 // Padding byte after escape to fill buffer to UTF-8 wire cost // ======== LARGE 12x12 SPRITES ======== static const uint8_t emoji_lg_wireless[] PROGMEM = { 0x00,0x00, 0x3F,0xC0, 0x60,0x60, 0xC0,0x30, 0x0F,0x00, 0x19,0x80, 0x30,0xC0, 0x00,0x00, 0x06,0x00, 0x0F,0x00, 0x06,0x00, 0x00,0x00, }; static const uint8_t emoji_lg_infinity[] PROGMEM = { 0x00,0x00, 0x00,0x00, 0x61,0x80, 0x92,0x40, 0x8C,0x40, 0x8C,0x40, 0x92,0x40, 0x61,0x80, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, }; static const uint8_t emoji_lg_trex[] PROGMEM = { 0x03,0xE0, 0x06,0xA0, 0x07,0xE0, 0x0C,0x00, 0x5C,0x00, 0x7C,0x00, 0x3C,0x00, 0x38,0x00, 0x3C,0x00, 0x36,0x00, 0x22,0x00, 0x33,0x00, }; static const uint8_t emoji_lg_skull[] PROGMEM = { 0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0, 0x40,0x20, 0x49,0x20, 0x2F,0x40, 0x1F,0x80, 0x96,0x90, 0x66,0x60, 0x36,0xC0, 0x96,0x90, }; static const uint8_t emoji_lg_cross[] PROGMEM = { 0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0, 0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x0F,0x00, }; static const uint8_t emoji_lg_lightning[] PROGMEM = { 0x03,0x00, 0x07,0x00, 0x0E,0x00, 0x1C,0x00, 0x3F,0x80, 0x01,0x80, 0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00, 0x30,0x00, 0x00,0x00, }; static const uint8_t emoji_lg_tophat[] PROGMEM = { 0x00,0x00, 0x1F,0x80, 0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0, 0x7F,0xE0, 0xFF,0xF0, 0xFF,0xF0, 0x00,0x00, }; static const uint8_t emoji_lg_motorcycle[] PROGMEM = { 0x00,0x00, 0x00,0x00, 0x03,0x80, 0x1F,0xC0, 0x3F,0xC0, 0x7F,0xC0, 0xFF,0xE0, 0xDF,0x60, 0x51,0x40, 0xE0,0xE0, 0x40,0x40, 0x00,0x00, }; static const uint8_t emoji_lg_seedling[] PROGMEM = { 0x00,0x00, 0x30,0x00, 0x79,0x80, 0x7B,0xC0, 0x33,0xC0, 0x1F,0x80, 0x06,0x00, 0x06,0x00, 0x06,0x00, 0x06,0x00, 0x00,0x00, 0x00,0x00, }; static const uint8_t emoji_lg_flag_au[] PROGMEM = { 0x00,0x00, 0x32,0x40, 0x4A,0x40, 0x4A,0x40, 0x7A,0x40, 0x4A,0x40, 0x49,0x80, 0x00,0x00, 0xFF,0xF0, 0x00,0x00, 0xFF,0xF0, 0x00,0x00, }; static const uint8_t emoji_lg_umbrella[] PROGMEM = { 0x06,0x00, 0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0, 0xFF,0xF0, 0xDB,0x70, 0x06,0x00, 0x06,0x00, 0x06,0x00, 0x06,0x00, 0x46,0x00, 0x3C,0x00, }; static const uint8_t emoji_lg_nazar[] PROGMEM = { 0x1F,0x80, 0x20,0x40, 0x4F,0x20, 0x99,0x90, 0xB6,0xD0, 0xB6,0xD0, 0xB6,0xD0, 0x99,0x90, 0x4F,0x20, 0x20,0x40, 0x1F,0x80, 0x00,0x00, }; static const uint8_t emoji_lg_globe[] PROGMEM = { 0x1F,0x80, 0x34,0xC0, 0x66,0x60, 0x4F,0x20, 0x8E,0x10, 0x86,0x10, 0x80,0x30, 0x46,0x60, 0x43,0xE0, 0x30,0xC0, 0x1F,0x80, 0x00,0x00, }; static const uint8_t emoji_lg_radioactive[] PROGMEM = { 0x00,0x00, 0x22,0x40, 0x32,0xC0, 0x32,0xC0, 0x1B,0x40, 0x00,0x00, 0x0F,0x00, 0x0F,0x00, 0x00,0x00, 0x60,0x20, 0x39,0xC0, 0x0F,0x00, }; static const uint8_t emoji_lg_cow[] PROGMEM = { 0x00,0x00, 0xC0,0x60, 0x6E,0xC0, 0x3F,0x80, 0x2A,0x80, 0x3F,0x80, 0x3F,0x80, 0x7F,0xC0, 0x5F,0x40, 0x5F,0x40, 0x11,0x00, 0x31,0x80, }; static const uint8_t emoji_lg_alien[] PROGMEM = { 0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0, 0x76,0xE0, 0xF6,0xF0, 0x96,0x90, 0x7F,0xE0, 0x36,0xC0, 0x3F,0xC0, 0x16,0x80, 0x0F,0x00, 0x06,0x00, }; static const uint8_t emoji_lg_invader[] PROGMEM = { 0x10,0x80, 0x09,0x00, 0x1F,0x80, 0x36,0xC0, 0x7F,0xE0, 0x5F,0xA0, 0x50,0xA0, 0x50,0xA0, 0x19,0x80, 0x19,0x80, 0x30,0xC0, 0x00,0x00, }; static const uint8_t emoji_lg_dagger[] PROGMEM = { 0x00,0x20, 0x00,0x60, 0x00,0xC0, 0x01,0x80, 0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00, 0x38,0x00, 0x58,0x00, 0x28,0x00, 0x18,0x00, }; static const uint8_t emoji_lg_grimace[] PROGMEM = { 0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0, 0x40,0x20, 0x40,0x20, 0x5F,0xA0, 0x55,0x40, 0x5F,0xA0, 0x20,0x40, 0x1F,0x80, 0x00,0x00, }; static const uint8_t emoji_lg_telephone[] PROGMEM = { 0x00,0x00, 0x7F,0xE0, 0xC0,0x30, 0xC0,0x30, 0x60,0x60, 0x30,0xC0, 0x1F,0x80, 0x0F,0x00, 0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0, 0x00,0x00, }; static const uint8_t* const EMOJI_SPRITES_LG[] PROGMEM = { emoji_lg_wireless, emoji_lg_infinity, emoji_lg_trex, emoji_lg_skull, emoji_lg_cross, emoji_lg_lightning, emoji_lg_tophat, emoji_lg_motorcycle, emoji_lg_seedling, emoji_lg_flag_au, emoji_lg_umbrella, emoji_lg_nazar, emoji_lg_globe, emoji_lg_radioactive, emoji_lg_cow, emoji_lg_alien, emoji_lg_invader, emoji_lg_dagger, emoji_lg_grimace, emoji_lg_telephone, }; // ======== SMALL 10x10 SPRITES ======== static const uint8_t emoji_sm_wireless[] PROGMEM = { 0x00,0x00, 0x7F,0x80, 0xC0,0xC0, 0x1E,0x00, 0x33,0x00, 0x21,0x00, 0x00,0x00, 0x0C,0x00, 0x0C,0x00, 0x00,0x00, }; static const uint8_t emoji_sm_infinity[] PROGMEM = { 0x00,0x00, 0x00,0x00, 0xE7,0x00, 0x99,0x00, 0x99,0x00, 0xA5,0x00, 0x42,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, }; static const uint8_t emoji_sm_trex[] PROGMEM = { 0x07,0x80, 0x0F,0x80, 0x0F,0x80, 0x58,0x00, 0x78,0x00, 0x38,0x00, 0x38,0x00, 0x3C,0x00, 0x24,0x00, 0x26,0x00, }; static const uint8_t emoji_sm_skull[] PROGMEM = { 0x3F,0x00, 0x61,0x80, 0x73,0x80, 0x40,0x80, 0x52,0x80, 0x3F,0x00, 0x3F,0x00, 0xED,0xC0, 0x6D,0x80, 0xAD,0x40, }; static const uint8_t emoji_sm_cross[] PROGMEM = { 0x1E,0x00, 0x1E,0x00, 0x3F,0x00, 0x3F,0x00, 0x3F,0x00, 0x1E,0x00, 0x1E,0x00, 0x1E,0x00, 0x1E,0x00, 0x1E,0x00, }; static const uint8_t emoji_sm_lightning[] PROGMEM = { 0x06,0x00, 0x0E,0x00, 0x1C,0x00, 0x3E,0x00, 0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00, 0x30,0x00, 0x00,0x00, }; static const uint8_t emoji_sm_tophat[] PROGMEM = { 0x00,0x00, 0x3F,0x00, 0x3F,0x00, 0x3F,0x00, 0x3F,0x00, 0x3F,0x00, 0x7F,0x80, 0xFF,0xC0, 0xFF,0xC0, 0x00,0x00, }; static const uint8_t emoji_sm_motorcycle[] PROGMEM = { 0x00,0x00, 0x00,0x00, 0x1F,0x00, 0x3F,0x00, 0x7F,0x00, 0xFF,0x80, 0xFF,0x80, 0xE3,0x80, 0xC1,0x80, 0x00,0x00, }; static const uint8_t emoji_sm_seedling[] PROGMEM = { 0x00,0x00, 0x70,0x00, 0x77,0x00, 0x77,0x00, 0x3F,0x00, 0x0C,0x00, 0x0C,0x00, 0x0C,0x00, 0x00,0x00, 0x00,0x00, }; static const uint8_t emoji_sm_flag_au[] PROGMEM = { 0x00,0x00, 0x75,0x00, 0x55,0x00, 0x75,0x00, 0x55,0x00, 0x53,0x00, 0x00,0x00, 0xFF,0xC0, 0xFF,0xC0, 0x00,0x00, }; static const uint8_t emoji_sm_umbrella[] PROGMEM = { 0x0C,0x00, 0x3F,0x00, 0x7F,0x80, 0xFF,0xC0, 0xF7,0xC0, 0x0C,0x00, 0x0C,0x00, 0x0C,0x00, 0x4C,0x00, 0x78,0x00, }; static const uint8_t emoji_sm_nazar[] PROGMEM = { 0x3F,0x00, 0x40,0x80, 0x9E,0x40, 0xBF,0x40, 0xAD,0x40, 0xBF,0x40, 0x9E,0x40, 0x4C,0x80, 0x3F,0x00, 0x00,0x00, }; static const uint8_t emoji_sm_globe[] PROGMEM = { 0x3F,0x00, 0x69,0x80, 0x4C,0x80, 0x9C,0x40, 0x8C,0x40, 0x80,0xC0, 0x4D,0x80, 0x67,0x80, 0x3F,0x00, 0x00,0x00, }; static const uint8_t emoji_sm_radioactive[] PROGMEM = { 0x00,0x00, 0x25,0x00, 0x25,0x00, 0x37,0x00, 0x00,0x00, 0x1E,0x00, 0x1E,0x00, 0x40,0x00, 0x73,0x80, 0x1E,0x00, }; static const uint8_t emoji_sm_cow[] PROGMEM = { 0x00,0x00, 0xC1,0x80, 0x7F,0x00, 0x3F,0x00, 0x3F,0x00, 0x7F,0x00, 0x7F,0x00, 0x7F,0x00, 0x36,0x00, 0x23,0x00, }; static const uint8_t emoji_sm_alien[] PROGMEM = { 0x3F,0x00, 0x7F,0x80, 0x7F,0x80, 0xED,0xC0, 0xAD,0x40, 0x7F,0x80, 0x3F,0x00, 0x3F,0x00, 0x1E,0x00, 0x0C,0x00, }; static const uint8_t emoji_sm_invader[] PROGMEM = { 0x33,0x00, 0x1E,0x00, 0x3F,0x00, 0x7F,0x80, 0x7F,0x80, 0x61,0x80, 0x73,0x80, 0x33,0x00, 0x33,0x00, 0x00,0x00, }; static const uint8_t emoji_sm_dagger[] PROGMEM = { 0x00,0x80, 0x01,0x80, 0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00, 0x30,0x00, 0x70,0x00, 0x70,0x00, 0x30,0x00, }; static const uint8_t emoji_sm_grimace[] PROGMEM = { 0x3F,0x00, 0x61,0x80, 0x73,0x80, 0x40,0x80, 0x40,0x80, 0x7F,0x80, 0x55,0x00, 0x7F,0x80, 0x3F,0x00, 0x00,0x00, }; static const uint8_t emoji_sm_telephone[] PROGMEM = { 0x00,0x00, 0xFF,0xC0, 0xC0,0xC0, 0xC0,0xC0, 0x61,0x80, 0x3F,0x00, 0x1E,0x00, 0x3F,0x00, 0x7F,0x80, 0x00,0x00, }; static const uint8_t* const EMOJI_SPRITES_SM[] PROGMEM = { emoji_sm_wireless, emoji_sm_infinity, emoji_sm_trex, emoji_sm_skull, emoji_sm_cross, emoji_sm_lightning, emoji_sm_tophat, emoji_sm_motorcycle, emoji_sm_seedling, emoji_sm_flag_au, emoji_sm_umbrella, emoji_sm_nazar, emoji_sm_globe, emoji_sm_radioactive, emoji_sm_cow, emoji_sm_alien, emoji_sm_invader, emoji_sm_dagger, emoji_sm_grimace, emoji_sm_telephone, }; // ---- Codepoint lookup for UTF-8 detection ---- struct EmojiCodepoint { uint32_t cp; uint32_t cp2; uint8_t escape; }; static const EmojiCodepoint EMOJI_CODEPOINTS[20] = { { 0x1F6DC, 0x0000, 0x01 }, { 0x267E, 0x0000, 0x02 }, { 0x1F996, 0x0000, 0x03 }, { 0x2620, 0x0000, 0x04 }, { 0x271D, 0x0000, 0x05 }, { 0x26A1, 0x0000, 0x06 }, { 0x1F3A9, 0x0000, 0x07 }, { 0x1F3CD, 0x0000, 0x08 }, { 0x1F331, 0x0000, 0x09 }, { 0x1F1E6, 0x1F1FA, 0x0A }, { 0x2602, 0x0000, 0x0B }, { 0x1F9FF, 0x0000, 0x0C }, { 0x1F30F, 0x0000, 0x0D }, { 0x2622, 0x0000, 0x0E }, { 0x1F404, 0x0000, 0x0F }, { 0x1F47D, 0x0000, 0x10 }, { 0x1F47E, 0x0000, 0x11 }, { 0x1F5E1, 0x0000, 0x12 }, { 0x1F62C, 0x0000, 0x13 }, { 0x260E, 0x0000, 0x14 }, }; // ---- Helper functions ---- static uint32_t emojiDecodeUtf8(const uint8_t* s, int remaining, int* bytes_consumed) { uint8_t b0 = s[0]; if (b0 < 0x80) { *bytes_consumed = 1; return b0; } if ((b0 & 0xE0) == 0xC0 && remaining >= 2) { *bytes_consumed = 2; return ((uint32_t)(b0 & 0x1F) << 6) | (s[1] & 0x3F); } if ((b0 & 0xF0) == 0xE0 && remaining >= 3) { *bytes_consumed = 3; return ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)(s[1] & 0x3F) << 6) | (s[2] & 0x3F); } if ((b0 & 0xF8) == 0xF0 && remaining >= 4) { *bytes_consumed = 4; return ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)(s[1] & 0x3F) << 12) | ((uint32_t)(s[2] & 0x3F) << 6) | (s[3] & 0x3F); } *bytes_consumed = 1; return 0xFFFD; } static void emojiSanitize(const char* src, char* dst, int dstLen) { const uint8_t* s = (const uint8_t*)src; int si = 0, di = 0; int srcLen = strlen(src); while (si < srcLen && di < dstLen - 1) { uint8_t b = s[si]; if (b >= 0xE0) { int consumed; uint32_t cp = emojiDecodeUtf8(s + si, srcLen - si, &consumed); if (cp == 0xFE0F) { si += consumed; continue; } bool found = false; for (int e = 0; e < 20; e++) { if (EMOJI_CODEPOINTS[e].cp == cp) { if (EMOJI_CODEPOINTS[e].cp2 != 0) { int consumed2; if (si + consumed < srcLen) { uint32_t cp2 = emojiDecodeUtf8(s + si + consumed, srcLen - si - consumed, &consumed2); if (cp2 == EMOJI_CODEPOINTS[e].cp2) { dst[di++] = EMOJI_CODEPOINTS[e].escape; si += consumed + consumed2; found = true; break; } } continue; } dst[di++] = EMOJI_CODEPOINTS[e].escape; si += consumed; if (si + 2 < srcLen && s[si] == 0xEF && s[si+1] == 0xB8 && s[si+2] == 0x8F) si += 3; found = true; break; } } if (!found) si += consumed; } else { dst[di++] = (char)b; si++; } } dst[di] = '\0'; } static inline bool isEmojiEscape(uint8_t b) { return b >= EMOJI_ESCAPE_START && b <= EMOJI_ESCAPE_END; } // Encode a Unicode codepoint as UTF-8 into dst, return bytes written static int emojiEncodeUtf8(uint32_t cp, uint8_t* dst) { if (cp < 0x80) { dst[0] = (uint8_t)cp; return 1; } else if (cp < 0x800) { dst[0] = 0xC0 | (cp >> 6); dst[1] = 0x80 | (cp & 0x3F); return 2; } else if (cp < 0x10000) { dst[0] = 0xE0 | (cp >> 12); dst[1] = 0x80 | ((cp >> 6) & 0x3F); dst[2] = 0x80 | (cp & 0x3F); return 3; } else { dst[0] = 0xF0 | (cp >> 18); dst[1] = 0x80 | ((cp >> 12) & 0x3F); dst[2] = 0x80 | ((cp >> 6) & 0x3F); dst[3] = 0x80 | (cp & 0x3F); return 4; } } // Reverse of emojiSanitize: convert escape bytes back to UTF-8 emoji sequences // Used before sending over mesh/BLE so other devices and apps see real emoji // dst must be large enough (worst case: srcLen * 8 for all flag emoji) static void emojiUnescape(const char* src, char* dst, int dstLen) { int si = 0, di = 0; int srcLen = strlen(src); while (si < srcLen && di < dstLen - 1) { uint8_t b = (uint8_t)src[si]; if (b == EMOJI_PAD_BYTE) { si++; // Skip padding bytes continue; } if (isEmojiEscape(b)) { int idx = b - EMOJI_ESCAPE_START; if (idx < 20) { uint8_t utf8[8]; int len = emojiEncodeUtf8(EMOJI_CODEPOINTS[idx].cp, utf8); // Encode second codepoint if present (flag sequences) if (EMOJI_CODEPOINTS[idx].cp2 != 0) { len += emojiEncodeUtf8(EMOJI_CODEPOINTS[idx].cp2, utf8 + len); } if (di + len < dstLen) { memcpy(dst + di, utf8, len); di += len; } else break; // No room } si++; } else { dst[di++] = src[si++]; } } dst[di] = '\0'; } // Get large sprite (for compose/picker) static inline const uint8_t* getEmojiSpriteLg(uint8_t escape_byte) { if (!isEmojiEscape(escape_byte)) return nullptr; return (const uint8_t*)pgm_read_ptr(&EMOJI_SPRITES_LG[escape_byte - EMOJI_ESCAPE_START]); } // Get small sprite (for channel view) static inline const uint8_t* getEmojiSpriteSm(uint8_t escape_byte) { if (!isEmojiEscape(escape_byte)) return nullptr; return (const uint8_t*)pgm_read_ptr(&EMOJI_SPRITES_SM[escape_byte - EMOJI_ESCAPE_START]); } // Get the UTF-8 wire cost in bytes for an escape byte (4 for most emoji, 8 for flags) static inline int emojiUtf8Cost(uint8_t escape_byte) { if (!isEmojiEscape(escape_byte)) return 1; // not an emoji, regular char int idx = escape_byte - EMOJI_ESCAPE_START; uint32_t cp = EMOJI_CODEPOINTS[idx].cp; int cost = (cp < 0x80) ? 1 : (cp < 0x800) ? 2 : (cp < 0x10000) ? 3 : 4; if (EMOJI_CODEPOINTS[idx].cp2 != 0) { uint32_t cp2 = EMOJI_CODEPOINTS[idx].cp2; cost += (cp2 < 0x80) ? 1 : (cp2 < 0x800) ? 2 : (cp2 < 0x10000) ? 3 : 4; } return cost; }