LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


Unicode 6.0を含めた絵文字変換を実現する

こんにちは。検索サービス開発2チームの斎藤です。休日は都内の美術館や博物館を巡り歩いています。
先日は池袋の古代オリエント博物館に行き、ハムラビ法典(のレプリカ)を見てきました。楔形文字はアシの筆を粘土板に押し当てて記述するものですが、ハムラビ法典は閃緑岩の石柱に彫られたそうです。「法典は石柱に彫ってね」と役人に無茶ぶりされて、当時の職人も「用途がちがーう」とか愚痴ったのかなぁ・・・と妄想してしまいました。さて私の普段の業務ですが、NAVER LINEプロジェクトで絵文字やスタンプ関連の開発に携わっています。ちょうど楔形文字の話もしたところですので、このエントリでも絵文字の変換処理について解説させていただきます。

ドコモ/au/ソフトバンクの携帯電話(以下、フィーチャーフォン)で長く使われてきた絵文字も、2010年にUnicodeコンソーシアムによってUnicode 6.0で正式に定義されました。これまでは各キャリア間のコード変換を意識していればよかったのですが、今後はこのUnicode 6.0の定義との変換も含めて考慮しなければなりません。

変換テーブルを用意する

まずは絵文字コードの対応表を作るところから始めます。
以前は各キャリアが提供しているコード定義表を集約して、自前で対応表を作る必要がありました。
しかし現在は、Unicode 6.0に絵文字が採用される過程で作成された対応表があるので、ベースとしてその対応表を使わせていただくことにします。必要に応じて、差分だけ修正していきます。

Unicodeの対応表

まずUnicode.orgの対応表を確認してみましょう。
Emoji Symbols: Background Data
Unicodeで採用されたコードポイントと、各キャリアとGoogleが定義したコードポイントとの対応が記載されています。以下に一部を抜粋します。

Unicode.orgの絵文字対応表1

Unicode 6.0で定義されている絵文字(以下、6.0標準)では、合字で定義されている絵文字に注意してください。
たとえば携帯電話のテンキーを表す絵文字ですが、Unicodeでは半角の数値文字(U+0031〜)と囲み文字(U+20E3)の組み合わせによって定義されています。

Unicode.orgの対応表2-テンキー

国旗の絵文字も同様で、フィーチャーフォンでは1コードポイントに定義されているのに対して、6.0標準ではREGIONAL INDICATOR SYMBOL LETTERS(地域識別用の記号)の組み合わせとして定義されています。

Unicode.orgの絵文字対応表3_国旗の例

このように、表示上の1文字を2つのUnicodeコードポイントで表現するケースがあることを考慮する必要があります。

基本となる定義はこのHTMLに収まっていますが、プログラムで処理しやすいフォーマットが欲しいところですよね。それについてはGoogle Codeのemoji4unicodeプロジェクトでXMLとして生成済みのため、今回はこれを使わせていただきます。(※ライセンスはApache License 2.0です)

http://code.google.com/p/emoji4unicode/source/browse/trunk/data/emoji4unicode.xmlXMLにあるように、絵文字を表示できないケースのためにUnicodeコンソーシアムではFallback(代替テキスト)を同時に定義しています。
たとえばソフトクリームの絵文字を見てみましょう。au/ソフトバンクには絵文字として定義されていますが、ドコモでは定義されていません。ドコモでは代わりに何か表示をしたいところなので、このときにFallbackテキスト"[ソフトクリーム]"を利用できます。

先述のとおり、対応表にはGoogleが独自にPUA(Private Use Area, Unicode私用領域)に割り当てたコードも含まれています。6.0標準が合字を使っているのに対して、Googleでは表示上の1文字に対して1つのコードポイントを対応付けています。合字のことを考慮しないで済むため、より現実に即した実践的な定義といえるでしょう。

Shift_JIS定義

フィーチャーフォン向けにエンコードをShift_JISに変換して出力したい場合、Shift_JISの定義についても取り込む必要があります。これもGoogle CodeプロジェクトにあるEmojiSources.txtを使わせていただきましょう。
http://code.google.com/p/emoji4unicode/source/browse/trunk/generated/EmojiSources.txt
6.0標準のコードポイントに対して、各キャリアのShift_JISコード値が定義されています。キー値である6.0標準のコードポイントには合字のケースも含まれているので、取り込む際にはその点を注意しましょう。

au出力用のコードを定義する

auに関してはエンコーディングについて特殊な事情があるため、絵文字のコード定義をもうひとつ生成します。
auが公開しているUTF-8用の絵文字コードポイントは、対応する文字をそのままHTMLに記述しても表示できません。XMLの文字参照表記(〹)で記述したときに表示可能です。
文字参照表記を使わずにUTF-8のページで表示するためには、Shift_JIS定義のコードから算出されるコードを使います。
こちらで検証していただいているように、Shift_JIS定義のコードから0x0700を減算した数値をUnicodeコードポイントとして出力すれば、文字参照表記でなくともUTF-8のページで表示できるようになります。
Shift_JIS定義のEmojiSources.txtを取り込む際に、このコード定義を生成しておきましょう。

対応表がひとまず完成

これらを組み合わせて、表示上の絵文字1文字について以下のコード定義(と代替テキスト)を対応づけられます。

  • Unicode 6.0標準
  • DOCOMO
  • AU
  • AU-UNICODE-OUT
  • SOFTBANK
  • GOOGLE
  • Fallback(代替テキスト)

まずはこの対応表をスタート地点とします。
用途に合わせて、不要なコード定義を削ったり独自の定義を追加したりしていきます。

どのコードをメインで使うか

この対応表を、アプリケーションで実際にどのように活用していけばよいでしょうか?
今回は、サーバー側ではある1つのコードに統一して保持し、クライアントに合わせて出力時のコードを変換するという方式で説明します。ではサーバー側で保持するにはどのコードが良いか、順にチェックしていきましょう。

各キャリアの定義

互いに欠けている絵文字があるため、統一コードとして使うのは無理があります。

6.0標準

サービスの国際化対応を考えると、これが一番良さそうです。合字のケースを考慮する必要がある点だけ注意です。

Google定義

表示上の1文字と1コードポイントが1:1で対応しており、合字を考慮しなくてよいためシンプルです。
注意点は、UnicodeのPUA(Private Use Area, 私用領域)のうちU+FE000以降に定義されている点です。独自で外字を使いたい場合などは、このU+FE000からの重複する範囲には定義できません。逆に自前のサービスで既にこの範囲に外字を定義済みの場合も、Google定義をそのまま使うと重複してしまいます。

Google定義をベースに新しくコードを定義する

Google定義そのままでも実践的で使いやすいのですが、これを変換して使うケースも検討します。
Googleの絵文字コードは、PUAのうちU+FE000の位置から定義されています。これについて下位16ビットだけに注目するとU+E000です。これは、BMP(U+0000〜U+FFFF)上のPUAであるU+E000〜U+F8FF内に対応付けられるようになっています。下位16ビットをもとに絵文字コード定義を作成できます。
UnicodeのU+10000以降の文字は、サロゲートペアのチェックやデータサイズの点で少々使いにくい部分があります。絵文字をBMP上に対応させてしまえば、それらの手間が省けて扱いやすくなります。

BMP範囲外(U+10000〜)のUnicodeの注意点

Google定義の絵文字にかぎらず、コードポイントがU+10000以降のUnicodeの文字を扱う場合には注意すべき点があります。
UTF-8の場合、BMPでは1文字につき3バイト以内で表現できましたが、BMP外の文字は4バイト必要になります。UTF-16の場合、サロゲートペアを使って表現しなければなりません。2文字分4バイトのサイズが必要です。
UTF-16では文字列の長さを測る場合も注意してください。表示上では1文字でも、サロゲートペアの場合は2文字として計算されてしまいます。JavaのStringクラスであれば、 #length() と #codePointCount() を正しく使い分けなければいけません。
また、処理系が対応していないケースがあります。たとえばMySQL 5.1のutf8指定では、BMP範囲(U+0000〜U+FFFF, UTF-8で3バイト範囲)のUnicodeしか扱えないという制限があります。この場合は1つの手段としてバイナリ型に置き換えれば保持できます。 「枯れた(安定した)モジュールのみで構築したい」等、運用環境が限定される局面もありますので、BMP範囲外のUnicodeが扱えるか事前にチェックしておきましょう。

自前で定義しなおす

先述のとおり、PUAには既存サービスで先に特殊文字を割り当ててしまったのでGoogle定義と重複してしまう・・・といったケースもあるでしょうから、その場合は自前で定義が必要になるでしょう。
その場合でも、6.0標準やGoogle定義をベースとして、必要な部分だけPUA空き領域を活用して再定義するという方法が有効です。

ここまで各コード定義についてざっと見てきましたが、サーバー内部のデータ管理ではGoogle定義(or それをBMPに変換したもの)を使い、クライアントに合わせて出力コードを変換する、という方針をオススメします。

実装

ここまで整理できたら、実際にコードに落としこんでいきます。Javaで実装する場合の注意点を見ていきます。

Javaの内部エンコーディングはUTF-16

Javaの内部エンコーディングはUTF-16です。char型で表現されているのは、UTF-16エンコードされた文字データです。BMP範囲内は、表示1文字に対して1charが対応します。しかし絵文字をはじめとするコードポイントU+10000以上の文字については、サロゲートペア(D800〜D8FFとDC00〜DFFFの組)で表現されます。表示1文字につき2charが対応します。
Stringインスタンスはcharの配列で文字列情報を保持しています。String#length() メソッドの戻り値はこのchar配列長に相当しますので、サロゲートペアの分だけ表示上の文字数とズレてしまいます。表示上の文字数単位で処理する場合は String#codePointCount() メソッドを使います。

絵文字変換処理をするにあたって、char配列をそのまま走査して絵文字変換を行うこともできますが、その場合サロゲートペアのチェックと絵文字コードのチェックを同時に行うことになってしまいます。一度charの配列からUnicodeコードポイントの配列に変換した後で、そのコードポイントの配列に対して絵文字変換処理を行うとロジックをシンプルにできます。

Javaにおけるサロゲートペアの扱いに関しては、以下の解説が非常に役立ちます。Javaで実装するなら一度目を通しておくことをオススメします。
IBM developerWorks: Java による Unicode サロゲートプログラミング
こちらの例に従って、コードポイント列(int[])に変換します。

コード例

下記の例では、 EmojiMappingクラスが1文字ごとのコードの対応を表しています。
EmojiConverter#convert()メソッドには、合字チェックも含めた変換ロジックを記載します。

/** コード種別 */
public enum CodeType {
    STANDARD,
    GOOGLE,
    GOOGLE_BMP_PRIVATE,
    DOCOMO,
    AU,
    AU_UTF8,
    SOFTBANK
}
public class EmojiMapping { /** 合字を考慮する場合、2コードポイントで1定義となるので64ビット必要。* 64ビット 0010FFFF_0010FFFF のうち、* 上位32ビットを第1コードポイント* 下位32ビットを第2コードポイント* として保持する。* 例:日本国旗は6.0標準では"U+1F1EF U+1F1F5"の合字。*   これをlong値 0x0001f1ef_0001f1f5 とする*/
    private long standard;
    private long google;
    private long google_bmp_private; < /p>
    // 各キャリアでも合字のケースがあるため、starndard同様longで保持
    private long docomo;
    private long au;
    private long au_utf8;
    private long softbank;

    private String fallback;

    /** @return 指定コード種別に対応するコードポイントを、Stringに変換して返す */
    public String get(CodeType codeType) {
        return xxx;
    }
} /** 絵文字対応を管理するテーブル*/
public class EmojiTable {
    EmojiMapping get(int cp1, CodeType srcType) { ...
    }
    EmojiMapping get(int cp1, int cp2, CodeType srcType) { ...
    }
}
public class EmojiConverter {
    private EmojiTable table = EmojiTable.getInstance(); // 絵文字対応表のインスタンス

    public String convert(String srcString, CodeType srcType, CodeType destType) {

        // UTF-16のchar配列からコードポイントの配列に変換
        // IBM developerWorksのコードを参考に実装
        int[] cps = toCodePointArray(srcString);

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i & amp; lt; cpLen;) {
            final int cp1 = cps[i];
            i++;
            if (i & amp; lt; cpLen) {
                // 配列の末尾に達していなければ、2文字単位で合字チェック
                final int cp2 = cps[i];
                final EmojiMapping mappingPair = table.get(cp1, cp2, srcType);
                if (mappingPair == null) {
                    // 2コードポイント単位では合致するマッピングがない場合、
                    // はじめの1コードポイントだけでマッピング検索
                    final EmojiMapping mappingSingle = table.get(cp1, srcType);
                    if (mappingSingle == null) {
                        // 対応する絵文字がないのでそのまま出力
                        sb.appendCodePoint(cp1);
                    } else {
                        String destEmoji = mappingSingle.get(destType);
                        sb.append(destEmoji);
                    }
                } else {
                    // 2コードポイントで合致するマッピングがあった場合
                    String destEmoji = mappingPair.get(destType);
                    sb.append(destEmoji);
                    i++; // 2コードポイントを変換したのでインデックスをさらにインクリメント
                }

            } else {
                // 配列の末尾(最後の1コードポイント)
                // 省略
            }
        }
        return sb;
    }
}

まず2文字単位で合字チェック →絵文字対応表に定義がなければ1文字単位でチェック →定義があれば出力先のコード定義に合わせて置き換え・・・という地道な処理をしているだけですね。
NAVER LINEにおいても基本は上記のような実装ですが、出力コードが存在しない場合は
EmojiMapping#get(destType)メソッドがFallbackテキストを返すようにしています。
これで2文字の合字のケースも含めて変換が実現できます。

Fallbackテキストから絵文字を復元したい場合はもう少しロジックが複雑になりますが、今回は割愛させていただきます。

今後の課題

これで基本となる絵文字変換処理は実現できました。注意点と改良点を見ていきます。

字形の意味

Unicode採用以前からの絵文字の問題点として、字形の違いによって意味が異なってしまうというものがあります。いくつか例を見ていきます。
まず、ドコモの絵文字では地下鉄を表す絵文字がアルファベットのMをベースとしています。
端末のユーザーはこれをファストフード店を表す絵文字として使うケースがあるそうです。他キャリアでは、ストレートに電車の外観を表した字形で表現されています。そのためドコモユーザーが「マクドナルドでご飯」のつもりで送っても、他キャリアユーザーが受け取ると「電車でご飯」として表現されてしまうことになります。

ドコモでは地下鉄はアルファベットの「M」をベースとしている
iPhone/ソフトバンクでは、地下鉄の絵文字は電車の車両そのものの字形

別の例では、マフラー着用を表現しようと雪ダルマの絵文字を送ったが、受信者のフォントでは帽子を着けているケースもありえます。ドコモやUnicode標準では何も着けていないので、全裸になってしまいますね。
コードの対応付けよりも送り手の表現を優先するなら、絵文字ではなく、送り手の字形に準じた画像に置き換えて表示する策もあります。(そこまでやるともはや絵"文字"ではなくなってしまいますが。)

テーブル定義を手直しする

基本となる変換は先の対応表を使うにしても、Fallbackテキストをキャッチーなものに変えたりできるでしょう。また、HTML出力時に画像変換するのであれば、対応する画像IDを変換定義に追加しておくと便利です。

iPhoneの絵文字対応

iPhone4までは、ソフトバンクのコード定義に変換すれば絵文字を表示できました。しかしiPhone5から変更となる可能性があります。Mac OSX LionではUnicode 6.0に対応したため、6.0標準のコードポイントで絵文字を入力できます。iPhone5(iOS 5)でもUnicode 6.0をベースとした絵文字定義になると予想されます。(※本エントリ執筆時点では、iOS 5のUnicode対応に関するApple公式の情報を見つけられませんでした。そのため断言はできません・・・)クライアントのiOSバージョンをチェックして、出力コードを切り替える必要があるかもしれません。

Androidの絵文字対応

Androidで絵文字を扱う場合、いくつか問題があります。
AndroidはGoogleが提供しているのだから、Androidの絵文字もGoogle定義のコードで送信される・・・と言いたいのですが例外があります。マーケットには、キャリア定義のコードで絵文字を入力できるIMEが公開されています。Androidアプリを提供する場合には専用の絵文字入力パッドまで合わせて作りこみ、クライアントのコード定義を一定にするという対応も必要かもしれません。
また、将来AndroidもUnicode 6.0に対応するとなった場合もどうなるか。その時にどのコード定義を使うべきか再度確認が必要になるでしょう。

おわりに

このようにして、Unicode 6.0も含めた絵文字変換は実現できます・・・といっても、NAVER独自で特殊な処理を行っている部分はほとんどありません。対応表の作成やau独自のUTF-8変換についても、先行する多くのエンジニアの皆様の成果の上に成り立っています。今回のエントリではそれらの技術をつなぎ合わせて基礎を築いた段階ですので、まだまだ改良が必要になると思います。

絵文字がUnicodeに採用されたことで、今後は全世界で絵文字が利用されることになるでしょう。絵文字の開発にあたってこのエントリが役立てば幸いです。

参考資料

  • 深澤千尋『改訂第2版 文字コード超研究』(2011年、ラトルズ) ・・・Unicodeを含め、文字コードについて詳細に記載されています。雪ダルマの字形の話はこちらを参照させていただきました
  • Amebaにおける絵文字 ・・・メトロ記号の話はこちらから参照させていただきました
  • Unicode Fonts for Ancient Scripts ・・・Unicodeのフォントセットが公開されています。冒頭の楔形文字のみならず錬金術記号や麻雀牌も定義されており、眺めているだけでも面白いです
  • 特集 : 絵文字が開いてしまった「パンドラの箱」 ・・・Unicodeが絵文字に採用されるまでの紆余曲折、相互変換や字形の差異などの絵文字が抱える問題について解説されています。絵文字を扱うにあたっては一読することをオススメします。
  • ケータイの絵文字はどこまでズレるのか ・・・本文で紹介しきれていませんが、キャリア間で絵文字変換を繰り返す過程で意味が変容してしまうケースについて言及されています。