4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

コードポイントからの無難なUTF-8文字化

Posted at

先日のmb_strwidthの返す値が怪しかったので調べてみたで調べた情報を元にmb_strimwidthの移植を進めていたのだけど、想定しがたい箇所で結果の不一致が発生した。
JSとPHPで同じコードポイントを使った場合に同じ結果にするための方法について記載。

参考文献

PHP - コードポイントから UTF-8 の文字を生成する - Qiita

結論

JSはString.fromCharCode()メソッドを利用し、PHPは次の関数を利用すれば、同じコードポイントから同じ文字を得られる。

/**
 * 整数値で表現されたコードポイントをUTF-8文字に変換する。
 *
 * @param	int		$code_point	UTF-8文字に変換したいコードポイント
 * @return	string	コードポイントから作成したUTF-8文字
 */
function int2utf8($code_point) {
	//UTF-16コードポイント内判定
	if ($code_point < 0) {
		throw new \Exception(sprintf('%1$s is out of range UTF-16 code point (0x000000 - 0x10FFFF)', $code_point));
	}
	if (0x10FFFF < $code_point) {
		throw new \Exception(sprintf('0x%1$X is out of range UTF-16 code point (0x000000 - 0x10FFFF)', $code_point));
	}

	//サロゲートペア判定
	if (0xD800 <= $code_point && $code_point <= 0xDFFF) {
		throw new \Exception(sprintf('0x%X is in of range surrogate pair code point (0xD800 - 0xDFFF)', $code_point));
	}

	//1番目のバイトのみでchr関数が使えるケース
	if ($code_point < 0x80) {
		return chr($code_point);
	}

	//2番目のバイトを考慮する必要があるケース
	if ($code_point < 0xA0) {
		return chr(0xC0 | $code_point >> 6) . chr(0x80 | $code_point & 0x3F);
	}

	//数値実体参照表記からの変換
	return html_entity_decode('&#'. $code_point .';');
}

発端

当初は次の実装で比較を行っていたが、0x200あたりからズレが発生するようになった。

<?php
$code_point = dechex(512);    //0x200
$char_code = hexdec($code_point);
?>
実行結果:<?= mb_convert_encoding(pack('H*', $char_code), 'UTF-8', 'UCS-2'); ?><script type="text/javascript">document.write(String.fromCharCode(<?= $char_code ?>));</script>

実行結果:儠:Ȁ

全然違う文字やんけ!

UTF-8においてはASCIIの範囲内はコードポイントをそのまま使えるのですが、その先にあるLatin-1の範囲の処理に考慮が必要だったというオチ。

検証

簡易的に次の処理で目視で検証
:の前後の文字が同じならばOK

<html>
<head>
</head>
<body>

<?php
/**
 * 整数値で表現されたコードポイントをUTF-8文字に変換する。
 *
 * @param	int		$code_point	UTF-8文字に変換したいコードポイント
 * @return	string	コードポイントから作成したUTF-8文字
 */
function int2utf8($code_point) {
	//UTF-16コードポイント内判定
	if ($code_point < 0) {
		throw new \Exception(sprintf('%1$s is out of range UTF-16 code point (0x000000 - 0x10FFFF)', $code_point));
	}
	if (0x10FFFF < $code_point) {
		throw new \Exception(sprintf('0x%1$X is out of range UTF-16 code point (0x000000 - 0x10FFFF)', $code_point));
	}

	//サロゲートペア判定
	if (0xD800 <= $code_point && $code_point <= 0xDFFF) {
		throw new \Exception(sprintf('0x%X is in of range surrogate pair code point (0xD800 - 0xDFFF)', $code_point));
	}

	//1番目のバイトのみでchr関数が使えるケース
	if ($code_point < 0x80) {
		return chr($code_point);
	}

	//2番目のバイトを考慮する必要があるケース
	if ($code_point < 0xA0) {
		return chr(0xC0 | $code_point >> 6) . chr(0x80 | $code_point & 0x3F);
	}

	//数値実体参照表記からの変換
	return html_entity_decode('&#'. $code_point .';');
}
?>

<?php foreach (range(1, 512) as $char_code) { ?>
<?= int2utf8($char_code) ?><script type="text/javascript">document.write(String.fromCharCode(<?= $char_code ?>));</script>
<br />
<?= $char_code ?>:document.write(String.fromCharCode(<?= $char_code ?>));
<hr />
<?php } ?>

</body>
</html>
4
3
1

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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?