Files
radiantwave/src/utf8_utils.h
T
2026-08-07 14:51:41 -05:00

130 lines
3.8 KiB
C++

#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 <cstdint>
#include <string>
#include <utility>
#include <vector>
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<uint32_t>& out)
{
std::vector<uint32_t> result;
const char* p = text.data();
const char* end = p + text.size();
while (p < end)
{
const uint8_t lead = static_cast<uint8_t>(*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<size_t>(end - p) < extra + 1)
{
return false;
}
for (size_t i = 1; i <= extra; ++i)
{
const uint8_t cc = static_cast<uint8_t>(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<char>(cp));
}
if (cp < 0x800)
{
return {static_cast<char>(0xC0 | (cp >> 6)),
static_cast<char>(0x80 | (cp & 0x3F))};
}
if (cp < 0x10000)
{
return {static_cast<char>(0xE0 | (cp >> 12)),
static_cast<char>(0x80 | ((cp >> 6) & 0x3F)),
static_cast<char>(0x80 | (cp & 0x3F))};
}
return {static_cast<char>(0xF0 | (cp >> 18)),
static_cast<char>(0x80 | ((cp >> 12) & 0x3F)),
static_cast<char>(0x80 | ((cp >> 6) & 0x3F)),
static_cast<char>(0x80 | (cp & 0x3F))};
}
} // namespace radiantwave