#pragma once // --------------------------------------------------------------------------- // UTF-8 helpers for the per-glyph scroller atlas. // // [This implementation] -- the scroller scrambles individual glyphs rather // than whole words (see scramble/fibonacci_scrambler.h and scroller_page.cpp), // so the word cache's string keys must be decomposed into independent code // points before scrambling and re-encoded one at a time for rasterization. // These helpers are that bridge. // // Because every valid code point is handled -- not just A-Za-z0-9 -- // punctuation such as backtick, comma, apostrophe, single/double quotes, em // dashes, and curly quotes all get their own scrambled tile. This is the // requested "include the most common non-alpha-numeric text characters" // behavior. // // The decoder is strict: it rejects overlong encodings, UTF-16 surrogate code // points, values above U+10FFFF, and malformed continuation bytes, so garbage // bytes never reach the font rasterizer. // --------------------------------------------------------------------------- #include #include #include #include namespace radiantwave { // Split `text` into its Unicode scalar values (code points), in order. // Returns false (leaving `out` untouched) when `text` is not valid UTF-8. inline bool splitUtf8(const std::string& text, std::vector& out) { std::vector result; const char* p = text.data(); const char* end = p + text.size(); while (p < end) { const uint8_t lead = static_cast(*p); uint32_t cp = 0; size_t extra = 0; uint32_t min = 0; if (lead < 0x80) { cp = lead; } else if ((lead & 0xE0) == 0xC0) { cp = lead & 0x1F; extra = 1; min = 0x80; } else if ((lead & 0xF0) == 0xE0) { cp = lead & 0x0F; extra = 2; min = 0x800; } else if ((lead & 0xF8) == 0xF0) { cp = lead & 0x07; extra = 3; min = 0x10000; } else { return false; } if (static_cast(end - p) < extra + 1) { return false; } for (size_t i = 1; i <= extra; ++i) { const uint8_t cc = static_cast(p[i]); if ((cc & 0xC0) != 0x80) { return false; } cp = (cp << 6) | (cc & 0x3F); } if (cp < min || cp > 0x10FFFF) { return false; // overlong or out of range } if (cp >= 0xD800 && cp <= 0xDFFF) { return false; // UTF-16 surrogate } result.push_back(cp); p += extra + 1; } out = std::move(result); return true; } // Encode a single Unicode scalar value back to UTF-8. Returns an empty string // for values outside the scalar range (surrogates, > U+10FFFF); callers treat // an empty result as "no glyph". inline std::string encodeUtf8(uint32_t cp) { if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) { return {}; } if (cp < 0x80) { return std::string(1, static_cast(cp)); } if (cp < 0x800) { return {static_cast(0xC0 | (cp >> 6)), static_cast(0x80 | (cp & 0x3F))}; } if (cp < 0x10000) { return {static_cast(0xE0 | (cp >> 12)), static_cast(0x80 | ((cp >> 6) & 0x3F)), static_cast(0x80 | (cp & 0x3F))}; } return {static_cast(0xF0 | (cp >> 18)), static_cast(0x80 | ((cp >> 12) & 0x3F)), static_cast(0x80 | ((cp >> 6) & 0x3F)), static_cast(0x80 | (cp & 0x3F))}; } } // namespace radiantwave