# 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 化を避けるため、見た目とテキストとしての扱いを両立できる。