130 lines
3.8 KiB
C++
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
|