AndroidアプリでPDFファイルを作成する方法

開発中のCamrepoというAndroidアプリで、PDFファイルを作成する必要があったので方法を調べました。この記事では、PDFファイルを作成するところまでを説明し、作成したファイルを保存する方法については「PDFファイルを共有ストレージに保存する方法」で説明しています。

PdfDocumentクラス

Android APIにはandroid.graphics.pdfというパッケージがあり、PDFファイルを作成するPdfDocumentクラスと、レンダリングするPdfRendererクラスが用意されています。PdfDocumentクラスはAPI Level 19以降、PdfRendererクラスはAPI Level 21以降で使用可能です。今回の目的はPDFファイルの作成ですので、PdfDocumentクラスを使用します。

PDFを作成する基本的な流れは、以下のようになります。

  1. ドキュメントを作成する
  2. ページの作成を開始する
  3. ページの内容を描画する
  4. ページの作成を完了する
  5. ドキュメントをストリームに書き出す
  6. ドキュメントを閉じる

2~4はページの数だけ繰り返します。スレッドセーフではないので、複数ページを同時に作成することはできません。単一スレッドで一ページずつ順に作っていく必要があります。

ここまでをソースコードにすると以下のようになります。drawPage()については後述します。引数のoutputStreamには、保存するファイルのFileOutputStreamなどを渡します。とりあえずテストのためにどこでもいいからファイルとして書き出したい、という場合の方法はこの記事の後半に書いています。ちゃんと他のアプリから見れる場所に保存する方法は、「PDFファイルを共有ストレージに保存する方法」で紹介していますのでご覧ください。

fun createPdf(outputStream: OutputStream) {
    // 1. ドキュメントを作成する
    val document = PdfDocument()

    val pageNum = 3
    // ページサイズは72dpiで指定する。A4なら595x842になる。
    val pageWidth = 595
    val pageHeight = 842

    // ページ数分繰り返す
    for (p in 0 until pageNum) {
        // 第3引数のpageNumberはPDF生成自体には使われていないっぽい。
        val pageInfo = PdfDocument.PageInfo.Builder(pageWidth, pageHeight, p).create()

        // 2. ページの作成を開始する
        val page = document.startPage(pageInfo)

        // 3. ページの内容を描画する
        drawPage(page)

        // 4. ページの作成を完了する
        document.finishPage(page)
    }

    // 5. ドキュメントをストリームに書き出す
    document.writeTo(outputStream)

    // 6. ドキュメントを閉じる
    document.close()
}

やや悩むのはPageInfoオブジェクトでしょうか。Pageオブジェクトのメタデータを格納するオブジェクトで、PageInfo.Builderクラスを使って作成し、startPage()に渡します。

Builder()の第一、第二引数はページサイズで、リファレンスに書いてある通り、72dpiで指定します。例えばA4サイズのPDFを作る場合、

210 x 297 (mm) ≒ 8.26 x 11.69 (inch) ≒ 595 x 842 (px)

となります。

第三引数はページ番号ですが、PDF生成自体には使われていないようです。リファレンスにも”The page number”とだけしか説明がなく、0始まりなのか1始まりなのかなども説明がありません。試しにでたらめな数値を指定してみたり、複数ページに同じ数値を指定してみたりと実験しましたが、作成されるPDFには何も影響がありませんでした。ということであまり深く考えず適当な数値を指定しても問題ないですが、この数値はPageInfo#getPageNumber()で取得することができるので、ページ番号によって処理を分けたい場合に活用できます。今回は2番目のページ描画方法の例で、Pageオブジェクトからページ番号を取得し、ページ番号に合わせた内容を描画しています。

ページ描画方法その1 – 作成済みのViewの状態をそのまま描画する

ここからはページのコンテンツを描画するdrawPage()の具体的な中身を見ていきます。大きく2つの方法があります。

一つ目は、レイアウト済みのViewの中身をそのままPDFのページとして描画する方法です。例えば、Activity全体をPDFに描画するには以下のようにします。

private lateinit var binding: ActivityMainBinding // DataBindingオブジェクト。

private fun drawPage(page: PdfDocument.Page) {
    val contentView = binding.root
    contentView.draw(page.canvas)
}

DataBindingやViewBindingを使っている場合、binding.rootでActivityのルートレイアウトを取得することができます。このレイアウトのdraw()メソッドにPdfDocument.Page#canvasを渡してあげれば、画面全体をそのまま描画することができます。もちろんActivity内の特定のViewだけを描画することも可能で、bindingオブジェクト経由、もしくはfindViewById()などで取得したViewのdraw()を呼び出せばOKです。

一見簡単なこの方法ですが、いくつか注意点があります。

  • ダークモード時は画面表示とPDF出力の色が同じにならない。
  • draw()を呼び出した瞬間のViewの状態が出力される。
  • Viewのサイズと同じサイズのPDFページを作成する必要がある。

以下の左の図のように、ボタンが一つある画面を作って実験してみました。ボタンを押すと画面全体をPDFとして出力します。出力したPDFは右の図です。ダークモードでは背景が黒くなっていますが、PDFでは白くなっています。また、ボタンを押したときのリップルエフェクトもPDFに出力されています。この実験はPixel4aで行ったので、Pixel4aの解像度1080×2340でPDFを作成すると全体が収まりましたが、もっと解像度が高いデバイスで同じサイズのPDFを出力すると、画面全体が収まらないはずです。ViewのサイズをPDFのサイズにうまく合わせこむ方法もあるかもしれませんが、今のところ調べ切れていません。

Viewの中身をそのまま描画する方法は、手軽にPDFを書き出すにはよさそうですが、細かいところをきちんと制御しようと思うとなかなか難易度が高そうです。

ページ描画方法 その2 – 手動でテキストや画像などを描画する

もう一つの方法は、Canvasにテキストや画像などを一つ一つ描いていく方法です。描画する内容に比例してソースコードは多くなりますが、コードを書いたとおりに見た目を制御できるので苦労は少ないと思います。

以下の例では、PageInfoに設定したページ番号を出力しています。

private fun drawPage(page: PdfDocument.Page) {
    val paint = Paint().apply {
        color = 0xff000000.toInt()
        textSize = 20f
    }
    page.canvas.drawText("Test PDF page.${page.info.pageNumber}", 100f, 100f, paint )
}

出力したPDF(3ページ分)がこちらです。この例では、最初のソースコードと同じ、A4サイズで作成しています。デバイスの解像度や画面の表示状態にかかわらず決まったサイズで描画をコントロールできるのがこの方法の良いところです。

PDFファイルを書き出すストリームの取得

document.writeTo()の引数には、PDFファイルを書き出すためのOutputStreamを渡します。OutputStreamは抽象クラスで、たくさんのサブクラスが用意されていますが、ファイルとして書き出す場合はFileOutputStreamが使えます。

とりあえず試すには、以下のようにアプリの内部ディレクトリに書き出すのが、パーミッション不要で楽です。ただし他のアプリからはアクセスできないので、作成したファイルを確認するにはAndroid StudioのDevice File Explorerなどを使う必要があります。

private fun getOutputStream(): OutputStream {
    val file = File(filesDir, "document.pdf") // Context#filesDirでアプリの内部ディレクトリのパスを取得
    return FileOutputStream(file)
}

ドキュメントをどこに保存するのがよいのか、またその保存方法については、「PDFファイルを共有ストレージに保存する方法」に書いています。

まとめ

今回はPDFファイルの作成方法について調べました。PdfDocumentクラスを使って、割と簡単にPDFファイルが作れることが分かりました。コンテンツの描画方法は二通り紹介しました。用途によるとは思いますが、きちんと見た目をコントロールしたい場合は、Canvasに手動でテキストや画像を描画していくやり方の方が思い通りの結果を得やすいのではないかと思います。