📕

FlutterでPDFを作成、保存、プレビューする

2021/11/25に公開

PDFを作成、保存、プレビューする

はじめに

FlutterでPDFを作成、保存、プレビューできる機能を搭載したアプリの制作を検討しており、PDF回りのライブラリの調査をしていました。色々と試した結果、私の用途では以下2つのライブラリを使用するのがベターという結論になったのでコードや所見など共有できればと思います。

  1. pdf
    PDFデータを「作成」するためのライブラリです。若干癖がありラーニングカーブがそこそこある気がしますが、PDFのコンテンツをflutter/materialライブラリのウィジェットを組む感覚で作成できるためアウトプットが想像しやすく、使い心地がいいです。

https://pub.dev/packages/pdf

  1. printing
    PDF含むドキュメントデータをプレビュー、印刷、共有等するためのライブラリです。PDFをプレビューするライブラリは他にもありますが、1. と同じ作者で相性もいいため、今回セットで使ってみることにしました。

https://pub.dev/packages/printing

これらをいじりながら作成したデモアプリがあるので、それに沿って解説させていただきます。
(参考ソースコード)
https://github.com/toshi-kuji/zenn-create-pdf

サンプル動作イメージ
サンプル動作イメージ

(主に使用したライブラリ)

  • pdf: ^3.6.3
  • printing: ^5.6.3

PDFデータを作成する

Documentオブジェクトの設定

「pdfライブラリ」では一つのPDFドキュメントのオブジェクトを Document というクラスで表しています。

そのDocumentが持つsaveメソッドを実行することでPDFのデータ(Uint8List)を生成することができ、Fileクラスなどに渡して「保存」が可能になります。

なので何はともあれ、まずはそのDocumentを作成して返してくれるファンクションを作るところから始めましょう。

services > pdf_creator.dart など
import 'package:pdf/widgets.dart';

class PdfCreator {
  static Future<Document> create() async {
    final pdf = Document(author: 'Me');
    // ページの作成
    // ページをDocumentに追加
    return pdf;
}

ファンクションの流れとしては、このDocumentに、addPageメソッドで「ページ」を追加していく形になります。

Documentのプロパティには様々な設定要素があり、圧縮の設定やメタデータなどの情報を付与できますが、ここではauthor(作者)情報を追加するだけに留めています。

Pageオブジェクトの設定

次に、表紙となるページを作成してみましょう。単体のページを作成するにはPageクラスを使用します。

services > pdf_creator.dart
    final cover = Page(
      pageTheme: PageTheme(), // テーマを設定
      build: (context) {
        // PDFコンテンツの本体部分であるWidgetを返す
      }
    );

Pageクラスで鍵となるプロパティはpageThemebuildのみです。それ以外のプロパティは結局すべてPageThemeの設定要素となるため、とりあえず無視して良いかと思います(pageThemeと「build以外の他のプロパティ」を同時に設定すると 開発時AssertionError が出ます)。

まずはpageThemeから設定してみたいと思います。

services > pdf_creator.dart
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart';
 // 省略
    final cover = Page(
+     pageTheme: PageTheme(
+       theme: ThemeData.withFont(base: font),
+       pageFormat: PdfPageFormat.a4,
+       orientation: PageOrientation.portrait,
+       buildBackground: (context) => Opacity(
+         opacity: 0.3,
+         child: FlutterLogo(),
+       ),
+     ), 
      build: (context) {
        // PDFコンテンツの本体部分であるWidgetを返す
      }
    );

PageThemeクラスではページサイズ(フォーマット)や向き(オリエンテーション)、背景、テキストテーマなどが設定できます。

ここではA4サイズ・縦向き、背景に (pdf/widgetsの)Opacity > FlutterLogoを設定しています。

PageTheme.themeに設定した(pdf/widgetsの)ThemeDataはMaterialのそれと異なり、主にテキストスタイルのテーマを表したものです。

Page.theme PageTheme.themeともに指定がない場合はデフォルトのスタイルが適用されますが、日本語をPDFにする場合はフォントの埋め込みが実質必須なので、ここで指定しておきます。

フォントの読み込みと設定

ThemeData.withFontThemeDataのすべてのテキストスタイルに一括でフォントを適用することができるfactoryコンストラクタです。baseの他にboldパラメータなどがあり、スタイル別にフォントを分けることも可能です。

フォントはThemeDataの設定の前に、rootBundleを使用してassetsフォルダから読み込んでおきます。(pubspec.yamlでのaasetsフォルダ登録を忘れずに!)

このデモではフォントにGoogleフォントのShipporiMinchoを使用しました。

services > pdf_creator.dart
class PdfCreator {
  static Future<Document> create() async {
    // フォントの読み込みとオブジェクト化
+   final fontData = await rootBundle.load('assets/ShipporiMincho-Regular.ttf');
+   final font = Font.ttf(fontData);

    final pdf = Document(author: 'Me');

    // 表紙の作成
    final cover = Page(
      pageTheme: PageTheme(
+       theme: ThemeData.withFont(base: font),
 // 省略

フォントのオブジェクト化には pdf/widgets の Fontクラス を使用します。

Font.courier() などのfacotryコンストラクタでフォントは指定できますが、日本語の場合はTTFフォントを埋め込む必要があるため、Font.ttfコンストラクタで読み込んだフォントデータを渡します。

内容が日本語なのにフォントの指定がなかったり、OTFフォントを指定したりするとエラーが発生するので注意です。

フォントの指定はPageオブジェクトごとに必要になります。

ちなみに、Googleフォントであればこの後PDFのプレビューで使用する printingライブラリPdfGoogleFontsクラスを使用して指定することも可能です!

フォントのDLを自動で行ってくれ、そのままFontオブジェクトを生成してくれるため、わざわざアセットを立てる必要がない上に一行で完結するので大変楽ですね。

(ただし、アセットから読み込んだ方がスピードは上だと思います。また、比較的新しいGoogleフォントはPdfGoogleFontsにないかもしれません)

services > pdf_creator.dart
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart';
+import 'package:printing/printing.dart';

class PdfCreator {
  static Future<Document> create() async {
    // フォントの読み込みとオブジェクト化
+   final Font font = await PdfGoogleFonts.shipporiMinchoRegular();

    final pdf = Document(author: 'Me');

    // 表紙の作成
    final cover = Page(
      pageTheme: PageTheme(
        theme: ThemeData.withFont(base: font),
 // 省略

Page.buildで本文を作成

ここまで設定したら、あとはbuildプロパティでウィジェット(pdf/widgets)を設定して、ページをDocumentオブジェクトに追加して返すだけです。

Page.buildで本文を作成 コード例
services > pdf_creator.dart
class PdfCreator {
  static Future<Document> create() async {
    final fontData = await rootBundle.load('assets/ShipporiMincho-Regular.ttf');
    final font = Font.ttf(fontData);

    final pdf = Document(author: 'Me');

    // 表紙
    final cover = Page(
      pageTheme: PageTheme(
        theme: ThemeData.withFont(base: font),
        pageFormat: PdfPageFormat.a4,
        orientation: PageOrientation.portrait,
        buildBackground: (context) => Opacity(
          opacity: 0.3,
          child: FlutterLogo(),
        ),
      ),
+     build: (context) => Center(
+       child: Column(
+         mainAxisAlignment: MainAxisAlignment.center,
+         children: [
+           Text(
+             BookData.title,
+             style: Theme.of(context).header0.copyWith(fontSize: 60),
+           ),
+           SizedBox(height: 30),
+           Text(
+             BookData.author,
+             style: Theme.of(context).header3,
+           ),
+         ],
+       ),
+     ),
    );

+   pdf.addPage(cover);
    return pdf;
  }
}

flutter/material と同じクラス名なので、どのようなアウトプットになるか想像がつきやすいですよね。

Theme.of(context) はもちろん、flutter/materialのThemeDataではなく、pdf/widgets のThemeData(テキストのスタイル群)を返します。

テキストデータはダミーです。冒頭のgithubリポジトリのdataフォルダを見ていただければわかると思いますが、青空文庫から「こころ」のデータを拝借しました。

このメソッドで作成したDocumentオブジェクトを save() してPDFとしてデータをDLもしくは作成するとこのような見た目になります。

こころ 夏目漱石
こころ 夏目漱石

MultiPageオブジェクトの設定

次に小説の本編部分をページオブジェクトにしたいと思います。単一ページに収まらない場合はPageクラスではなく、MultiPageクラスを使用します。

MultiPageオブジェクトの設定 コード例
services > pdf_creator.dart
class PdfCreator {
  static Future<Document> create() async {
 // 省略
+   final content = MultiPage(
      pageTheme: PageTheme(
        theme: ThemeData.withFont(base: font),
        pageFormat: PdfPageFormat.a4,
        orientation: PageOrientation.portrait,
      ),
      header: (context) {
        return Padding(
          padding: const EdgeInsets.only(bottom: 30),
          child: Text(BookData.title),
        );
      },
      footer: (context) {
        return Align(
          alignment: Alignment.centerRight,
          child: Text(BookData.author),
        );
      },
      build: (context) {
        return [
          Header(
            level: 0,
            textStyle: Theme.of(context).header0,
            child: Text(
              BookData.chapter1Title,
              style: Theme.of(context).header3,
            ),
          ),
          Paragraph(
            style: Theme.of(context).paragraphStyle,
            text: BookData.chapter1Body,
          ),
          Header(
            level: 0,
            textStyle: Theme.of(context).header0,
            child: Text(
              BookData.chapter2Title,
              style: Theme.of(context).header3,
            ),
          ),
          Paragraph(
            style: Theme.of(context).paragraphStyle,
            text: BookData.chapter2Body,
          ),
          Header(
            level: 0,
            textStyle: Theme.of(context).header0,
            child: Text(
              BookData.chapter3Title,
              style: Theme.of(context).header3,
            ),
          ),
          Paragraph(
            style: Theme.of(context).paragraphStyle,
            text: BookData.chapter3Body,
          ),
        ];
      },
    );
    
    pdf.addPage(cover);
+   pdf.addPage(content);
    return pdf;
}

Pageクラスと異なる点としては、headerfooterというコールバックがある点かと思います。

これらはWordなどでお馴染みのヘッダー、フッターの設定です。buildと同じく pdf/widgets を使用してウィジェットを作ります。

build内で使用している Header, Paragraph は flutter/material にはない pdf/widgets 独自のウィジェットです。

Header はいわゆる「見出し」、Paragraph は「段落」を描画するテキストウィジェットです。それぞれ styleプロパティ でPageThemeで設定したThemeDataのテキストスタイルを指定することができます。

また Header では levelプロパティ で見出しレベルを設定することができます。

ここで設定したMultiPageの見た目はこんな感じになります。

「こころ」PDFの本文ページ

「こころ」本文
「こころ」本文

これでPDFのデータを作成する機能が完成しました。これをファイル化してディレクトリに保存したりDLしたりする機能は別のテーマになるので省略させていただきます。

PDFデータをプレビューする

PDFのプレビュー表示は同じ作者による printingライブラリ を使用します。

使い方は簡単で、PdfPreviewというウィジェットのbuildメソッドでPDFデータの元となるUint8List(符号なし8ビット整数の配列)を生成するだけです。後は PdfPreview がデータの表示をしてくれます。

Uint8Listを生成するには先ほど PdfCreator.create() で作成した Documentsave() メソッドを使用します。

PDFデータをプレビューする コード例
views > preview_page.dart
import 'package:flutter/material.dart';
+import 'package:printing/printing.dart';

import '../services/pdf_creator.dart';
import '../services/save_helper/save_helper.dart';

class PreviewPage extends StatelessWidget {
  final String fileName;

  const PreviewPage(
    this.fileName, {
    Key? key,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(fileName),
      ),
+     body: PdfPreview(
        maxPageWidth: 600,
        allowPrinting: true,
        allowSharing: false,
        canChangeOrientation: false,
        canChangePageFormat: false,
        canDebug: false,
        loadingWidget: const LinearProgressIndicator(),
+       build: (format) async {
+         final pdf = await PdfCreator.create();
+         return await pdf.save();
+       },
        actions: [
          PdfPreviewAction(
            icon: const Icon(Icons.download),
            onPressed: (context, build, format) async {
              await SaveHelper.save(
                bytes: await build(format),
                fileName: fileName,
                platform: Theme.of(context).platform,
              );
            },
          ),
        ],
      ),
    );
  }
}

PdfPreview にもたくさんのプロパティがあります。一部ごく簡単に説明すると、、

  • maxPageWidth: PDF表示部分の横幅
  • allowPrinting: 印刷機能を使用するか
  • allowSharing: シェア機能を使用するか
  • canChangeOrientation: オリエンテーションを変更可能にするか
  • canChangePageFormat: ページサイズを変更可能にするか
  • loadingWidget: データロード中に表示するウィジェット(指定なしの場合はこれになる)

という感じです。

これでPDFを生成、プレビュー表示、保存(については興味がある方はgithubをご参照ください)する機能をFlutterで作ることができました。

サンプル動作イメージ
サンプル動作イメージ

(参考ソースコード)
https://github.com/toshi-kuji/zenn-create-pdf

最後に

本記事は以下のコンテンツを参考にさせていただきました。

ライブラリ公式のgithub(サンプル豊富で勉強になります)
https://github.com/DavBfr/dart_pdf/tree/master/demo/lib/examples

PDFとはなんぞ
https://www.antenna.co.jp/pdf/reference/FontEmbedding.html

ウェブ全盛の時代でもPDFは広く活用されているので、Flutterでもこのようなライブラリが利用できてとてもありがたく感じます。

https://github.com/DavBfr/dart_pdf/tree/master/pdf
https://pub.dev/packages/pdf

Discussion