FlutterでPDFを作成、保存、プレビューする
はじめに
FlutterでPDFを作成、保存、プレビューできる機能を搭載したアプリの制作を検討しており、PDF回りのライブラリの調査をしていました。色々と試した結果、私の用途では以下2つのライブラリを使用するのがベターという結論になったのでコードや所見など共有できればと思います。
-
pdf
PDFデータを「作成」するためのライブラリです。若干癖がありラーニングカーブがそこそこある気がしますが、PDFのコンテンツをflutter/materialライブラリのウィジェットを組む感覚で作成できるためアウトプットが想像しやすく、使い心地がいいです。
-
printing
PDF含むドキュメントデータをプレビュー、印刷、共有等するためのライブラリです。PDFをプレビューするライブラリは他にもありますが、1. と同じ作者で相性もいいため、今回セットで使ってみることにしました。
これらをいじりながら作成したデモアプリがあるので、それに沿って解説させていただきます。
(参考ソースコード)
サンプル動作イメージ
(主に使用したライブラリ)
-
pdf
: ^3.6.3 -
printing
: ^5.6.3
PDFデータを作成する
Documentオブジェクトの設定
「pdfライブラリ」では一つのPDFドキュメントのオブジェクトを Document というクラスで表しています。
そのDocumentが持つsaveメソッドを実行することでPDFのデータ(Uint8List)を生成することができ、Fileクラスなどに渡して「保存」が可能になります。
なので何はともあれ、まずはそのDocument
を作成して返してくれるファンクションを作るところから始めましょう。
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クラスを使用します。
final cover = Page(
pageTheme: PageTheme(), // テーマを設定
build: (context) {
// PDFコンテンツの本体部分であるWidgetを返す
}
);
Page
クラスで鍵となるプロパティはpageTheme
とbuild
のみです。それ以外のプロパティは結局すべてPageTheme
の設定要素となるため、とりあえず無視して良いかと思います(pageThemeと「build以外の他のプロパティ」を同時に設定すると 開発時AssertionError が出ます)。
まずはpageThemeから設定してみたいと思います。
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.withFontはThemeData
のすべてのテキストスタイルに一括でフォントを適用することができるfactoryコンストラクタです。base
の他にbold
パラメータなどがあり、スタイル別にフォントを分けることも可能です。
フォントはThemeDataの設定の前に、rootBundleを使用してassetsフォルダから読み込んでおきます。(pubspec.yamlでのaasetsフォルダ登録を忘れずに!)
このデモではフォントにGoogleフォントのShipporiMinchoを使用しました。
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にないかもしれません)
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で本文を作成 コード例
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オブジェクトの設定 コード例
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クラスと異なる点としては、header
とfooter
というコールバックがある点かと思います。
これらは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()
で作成した Document
の save() メソッドを使用します。
PDFデータをプレビューする コード例
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で作ることができました。
サンプル動作イメージ
(参考ソースコード)
最後に
本記事は以下のコンテンツを参考にさせていただきました。
ライブラリ公式のgithub(サンプル豊富で勉強になります)
PDFとはなんぞ
ウェブ全盛の時代でもPDFは広く活用されているので、Flutterでもこのようなライブラリが利用できてとてもありがたく感じます。
Discussion