はじめに
普段はMac環境で開発をしているため、Windows環境について考えることがあまりない。しかし最近、Windows 10で特定の絵文字が正しく表示されない問題があることに気づいた。
色々調べて、ユニコードに「サロゲートペア」という概念があり、このサロゲートペアを用いることで問題を解決できたため、記事として残してみた。
サロゲートペアとは
サロゲートペア(Surrogate Pair)は、直訳するとSurrogate(代理)のペアである。
何が代理?
これはUTF-16エンコーディングで使用される概念で、1つの文字を表現するために2つの16ビットコードユニットを使用する方法である。
例えば以下の地球の絵文字は一文字に見えるが、実際は長さ2の文字になっている。
const emoji = "🌍";
console.log(emoji.length); // 出力: 2
なぜ必要?
Unicodeは世界中のすべての文字を表現するために作られたけど、当初は16ビット(65,536文字)で十分だと考えられていた。
しかし、絵文字のような新しい文字が追加されるにつれて16ビットでは数が足りなくなって、サロゲートペアが導入されたことになる。
JavaScriptでのサロゲートペア
JavaScriptでは文字列をUTF-16でエンコードする。
例えば以下のような使い方ができる。
// サロゲートペアかどうかを確認する関数
function isSurrogatePair(str, index) {
const firstCode = str.charCodeAt(index);
if (firstCode > 0xD7FF && firstCode < 0xDC00) {
const secondCode = str.charCodeAt(index + 1);
return secondCode > 0xDBFF && secondCode < 0xE000;
}
return false;
}
// サロゲートペアかどうかを確認する関数
//(Intl.Segmenterを使用を利用するとマジックナンバーを使わずに実装できる)
const isSurrogatePairIntl = str => [...new Intl.Segmenter('ja', {granularity: 'grapheme'}).segment(str)].length !== str.length;
// コードポイントをサロゲートペアに変換
function toSurrogatePair(codePoint) {
codePoint -= 0x10000;
const highSurrogate = (codePoint >> 10) | 0xD800;
const lowSurrogate = (codePoint & 0x3FF) | 0xDC00;
return String.fromCharCode(highSurrogate, lowSurrogate);
}
Windows 10での問題
Windows 10でサポートされていない絵文字を表示しようとすると、「□」(tofuと呼ばれる)(かわいい)のような四角い文字が出力される。かわいいけど、絵文字の代わりに出されると文字化けになるため、せめて絵文字を見せていきたい。
代替文字出力のコード
ということでサロゲートペアを利用して、Unicodeを対応している場合はそのまま表示し、対応しない場合は代替の文字を表示するロジックを組んでみた。
// 絵文字がサポートされているか確認
function isEmojiSupported(emoji) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = '1px sans-serif';
const emojiWidth = ctx.measureText(emoji).width;
const fallbackWidth = ctx.measureText('□').width;
return emojiWidth !== fallbackWidth;
}
// 絵文字の代替マッピング(コードポイントを使用)
const emojiReplacements = {
0x1FAE5: 0x1F636, // 🫥 (点線の顔) -> 😶 (無表情の顔)
0x1FAE0: 0x1F643, // 🫠 (溶ける顔) -> 🙃 (逆さまの顔)
};
// 絵文字の表示
function renderEmoji(str) {
let result = '';
for (let i = 0; i < str.length; i++) {
if (isSurrogatePair(str, i)) {
let codePoint = str.codePointAt(i);
let emoji = String.fromCodePoint(codePoint);
if (!isEmojiSupported(emoji) && emojiReplacements[codePoint]) {
result += toSurrogatePair(emojiReplacements[codePoint]);
} else {
result += emoji;
}
i++; // サロゲートペアの2番目の部分をスキップ
} else {
result += str[i];
}
}
return result;
}
おわりに
もうWindows 11が出ているとは言え、WindowsでUnicode12までしか対応しないのはおかしいと思う。
個人アプリだと特に手元にWindowsの実証環境がないので、定期的に対応してほしい。