MySQL の utf8mb3 は基本多言語面外の文字 (U+10000 - U+10FFFF) をそのまま保存することができませんが、RFC 3629 では禁止されているサロゲートの範囲 (U+D800 - U+DFFF) の文字の利用が認められています。このような UTF-8 によく似たエンコーディングは CESU-8 と呼ばれ、UTR#26 で定義されます。たとえば、コードポイントの列である <U+004D, U+0061, U+F0000>
はバイト列で <4D 61 ED AE 80 ED B0 80>
とあらわされます。
CESU-8 を扱う場合の課題は標準関数やメソッド、主要なライブラリで対応していないため、不正なバイト列として削除されたり、代替文字 (U+FFFD) に置き換えられてしまう可能性が高いことです。実際のアプリケーションでは HTML 数値文字参照に変換する方法を採用したほうが安全でしょう。
ビット演算によるコードポイントと文字の相互変換については「コードポイントから UTF-8 の文字を生成する」および「UTF-8 の文字からコードポイントを求める」の記事 (「ビット演算の計算過程」) をご参照ください。
PHP
$input = "?野家";
$output = "\xED\xA1\x82"."\xED\xBE\xB7"."野家";
var_dump(
$output === UConverter::transcode($input, 'CESU-8', 'UTF-8'),
$input === UConverter::transcode($output, 'UTF-8', 'CESU-8'),
$output === utf8_to_cesu8($input),
$input === cesu8_to_utf8($output)
);
function utf8_to_cesu8($str)
{
$re = '/[^\x{0}-\x{FFFF}]/u';
return preg_replace_callback($re, function($m) {
$char = $m[0];
$x = ord($char[0]);
$y = ord($char[1]);
$z = ord($char[2]);
$w = ord($char[3]);
$cp = (($x & 0x7) << 18) | (($y & 0x3F) << 12) | (($z & 0x3F) << 6) | ($w & 0x3F);
// http://unicode.org/faq/utf_bom.html#utf16-4
$lead = 0xD800 - (0x10000 >> 10) + ($cp >> 10);
$trail = 0xDC00 + ($cp & 0x3FF);
return chr(0xE0 | $lead >> 12).chr(0x80 | $lead >> 6 & 0x3F).chr(0x80 | $lead & 0x3F).
chr(0xE0 | $trail >> 12).chr(0x80 | $trail >> 6 & 0x3F).chr(0x80 | $trail & 0x3F);
}, $str);
}
function cesu8_to_utf8($str)
{
$re = '/(\xED[\x80-\xBF]{2})(\xED[\x80-\xBF]{2})/xs';
return preg_replace_callback($re, function($m) {
$lead = ((ord($m[1][0]) & 0xF) << 12) | ((ord($m[1][1]) & 0x3F) << 6) | (ord($m[1][2]) & 0x3F);
$trail = ((ord($m[2][0]) & 0xF) << 12) | ((ord($m[2][1]) & 0x3F) << 6) | (ord($m[2][2]) & 0x3F);
if (0xD800 > $lead | $lead > 0xDBFF
| 0xDC00 > $trail | $lead > 0xDFFF) {
return $m[0];
}
// http://unicode.org/faq/utf_bom.html#utf16-4
$cp = ($lead << 10) + $trail + 0x10000 - (0xD800 << 10) - 0xDC00;
return chr(0xF0 | $cp >> 18).chr(0x80 | $cp >> 12 & 0x3F)
.chr(0x80 | $cp >> 6 & 0x3F).chr(0x80 | $cp & 0x3F);
}, $str);
}