LoginSignup
24
20

More than 5 years have passed since last update.

UTF-8 の4バイト文字を3バイト文字のペアに変換する

Last updated at Posted at 2015-01-21

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);
}
24
20
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
20