# Android PdfDocumentでEmojiCompatの絵文字をテキストとして描画する

Android の PdfDocument で、通常文字と絵文字が混在する文字列を描画する方法をまとめます。

# 目的

  • 単純絵文字を黒一色で表示します。
  • ZWJ 絵文字を分離させずに表示します。
  • 通常文字と絵文字が混在しても描画位置を維持します。
  • BitmapPath を使わず、PDF 上のテキスト選択とコピーを維持します。

# 方針

EmojiCompat.process() で文字列を処理し、付与された EmojiSpan を PDF の Canvas に直接描画します。

EmojiSpan.draw() に渡す PaintPorterDuffColorFilter を設定すると、カラー glyph を黒一色で描画できます。EmojiSpan を取得できない場合だけ、白黒絵文字フォントの Canvas.drawText() を補助的に使用します。

# 最小サンプル

このコードは、EmojiCompat の初期化完了後に呼び出します。androidx.emoji2:emoji2androidx.emoji2:emoji2-bundled が必要です。

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.pdf.PdfDocument;
import android.text.Spanned;

import androidx.emoji2.text.EmojiCompat;
import androidx.emoji2.text.EmojiSpan;

import java.io.IOException;
import java.io.OutputStream;

public final class EmojiTextPdf {
    public static void write(OutputStream output) throws IOException {
        if (EmojiCompat.get().getLoadState() != EmojiCompat.LOAD_STATE_SUCCEEDED) {
            throw new IllegalStateException("EmojiCompat の初期化完了後に実行してください");
        }

        PdfDocument document = new PdfDocument();
        PdfDocument.Page page = document.startPage(
                new PdfDocument.PageInfo.Builder(500, 200, 1).create());
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setTextSize(32);
        drawText(page.getCanvas(), "前🙂中😶‍🌫️後", 32, 100, paint);
        document.finishPage(page);
        document.writeTo(output);
        document.close();
    }

    private static void drawText(Canvas canvas, String text, float x, int baseline, Paint paint) {
        Spanned spanned = (Spanned) EmojiCompat.get().process(text);
        int index = 0;
        while (index < spanned.length()) {
            EmojiSpan[] spans = spanned.getSpans(index, index + 1, EmojiSpan.class);
            if (spans.length == 0 || spanned.getSpanStart(spans[0]) != index) {
                int next = spanned.nextSpanTransition(index, spanned.length(), EmojiSpan.class);
                String plain = spanned.subSequence(index, next).toString();
                canvas.drawText(plain, x, baseline, paint);
                x += paint.measureText(plain);
                index = next;
                continue;
            }

            EmojiSpan span = spans[0];
            int end = spanned.getSpanEnd(span);
            Paint.FontMetricsInt metrics = paint.getFontMetricsInt();
            int width = span.getSize(paint, spanned, index, end, metrics);
            Paint emojiPaint = new Paint(paint);
            emojiPaint.setColorFilter(new PorterDuffColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN));
            span.draw(canvas, spanned, index, end, x, baseline + metrics.ascent,
                    baseline, baseline + metrics.descent, emojiPaint);
            x += width;
            index = end;
        }
    }
}

# 実装時の注意点

  • PDF 出力は、EmojiCompat の LOAD_STATE_SUCCEEDED を確認してから開始します。
  • 中央寄せや右寄せでは、描画と同じ分岐で幅を計測します。
  • EmojiSpan.draw() の座標は baseline を基準に計算します。
  • PDF ビューアで、表示だけでなく選択とコピーも確認します。

この方法では Bitmap 化を避けられるため、見た目だけでなくテキストとしての扱いも維持できます。