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

PdfDocument で通常文字と絵文字が混在する文字列を描くと、ZWJ絵文字が分離したり、Bitmap化でコピーできなくなったりする。 EmojiCompat.process() で付いた EmojiSpan を Canvas に直接描き、Paint に PorterDuffColorFilter を設定すると、黒一色のテキストとして描画できる。

# 満たしたかった4つの要件

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

最後の要件があるため、Bitmap や Path に変換する方法は使わない。

# EmojiSpan をPDFの Canvas に直接描画する

EmojiCompat.process() で文字列を処理し、付与された EmojiSpan をPDFの Canvas に直接描画する。 EmojiSpan を取得できない場合だけ、白黒絵文字フォントの Canvas.drawText() を補助的に使う。

# PorterDuffColorFilter でカラーglyphを黒一色にする

EmojiSpan.draw() に渡す Paint へ PorterDuffColorFilter を設定する。 これで、カラーglyphを黒一色で描画できる。

このコードは、EmojiCompatの初期化完了後に呼び出す。 依存ライブラリとして androidx.emoji2:emoji2 と androidx.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出力前と出力後に確認すること

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

この方法は Bitmap 化を避けるため、見た目とテキストとしての扱いを両立できる。