見出し画像

【Flutter】「PDFの作成」及び「プレビューと印刷」


概要

Flutterアプリ開発における「PDF作成」及び「プレビュー/印刷」の実装についての備忘録を記す📝

アプリにPDFの機能を付ければ

・入力に応じPDFの作成
・特定フォーマットにおけるPDF出力
・結果一覧のPDF出力
・CSVなどを所定の表示にしてPDF出力
・PDF作成アプリ

などなど、開発の幅が一気に広がるであろう。
また、PDF出力のみ有料オプションにしても良いだろう。

ちなみに、記事を書きながら思ったのが「note」記事のエクスポート機能だが『PDF』形式にも追加対応してくれたら良いかもねと思った。

note記事のエクスポート。2024年9月時点では「WXR形式.xml」のみ

PDF機能の実装

検証環境

●検証環境
macOS Sonoma 14.3
VSCode 1.92.1
Flutter 3.22.3
Dart 3.4.3

※ 基本的なFlutter開発環境は整っている事を前提とします

プロジェクトを作成

●新規プロジェクトの作成
1.コマンドパレット(Command + Shift + P)を開き「flutter」と入力し、「Flutter: New Project」を選択。
2.一番上の「Application」を選択。
3.ワークスペースとなるディレクトリを選択(この中にプロジェクトが作成されます)

プロジェクト名は「note_flutter_pdf」とします(今回の記事では画像は省略)。

下準備

●ビルド対象
ビルド対象デバイスをWebにしておく。
(Android/iOSの動作確認は後でする)

・コマンドパレット(Command + Shift + P)を開き「flutter」と入力し、「Flutter: Select Device」を選択。

Chrome」を選択。

パッケージの導入

以下、2つのパッケージを導入する

pdf:PDFの作成で使用
printing:PDFの出力で使用

PDFを作成しても出力しなければ意味がないので、「pdf」と「printing」は基本、セットで導入します。

pubspec.yaml」のdependencies:ブロックに追記します

pdf: ^3.11.1
printing: ^5.13.2

pubspec.yaml」に追記

ウィジェット/インポート

●ウィジェット
PDFのコンテンツには、標準ウィジェットは使えません。
そのため、pdf/widgets.dartにある「PDF用ウィジェット」を使って作成します。

●インポート

import 'package:flutter/material.dart';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';

各パッケージのインポート。

pdf/widgets.dartですが、標準ウィジェットと名前が被ります。
衝突を避けるため、別名インポートします。
(  PDFコンテンツ作成には、「pw.XXXX」のように、PDF用ウィジェットを使う)

PDF作成

PDFを作成していきましょう。

●Documentの生成

final pdf = pw.Document();

まずは、Documentインスタンスの生成をします。

●ページ作成

final page = pw.Page(
  build: (pw.Context context) {
    return pw.Center(
      child: pw.Text("PDF Test"),
    ); // Center
  }
);

次にページを作成します。
ページコンテンツは「PDF用ウィジェット」にて作成していきます。

とは言え、ほぼ標準ウィジェットと同じ感覚で使えます。
違いと言えば、先ほど別名インポートした「pw」を使う(pw.XXXX)ぐらいでしょうか。

●ページ追加

pdf.addPage(page); 

ドキュメントにページを追加する

●関数化
PDF作成用の関数です。

Future makePdf() async {
  final pdf = pw.Document();
  final page = pw.Page(
    build: (pw.Context context) {
      return pw.Center(
        child: pw.Text("PDF Test"),
      ); // Center
    }
  );

  pdf.addPage(page); 

  return pdf;
}

単に関数にしただけです。
PDFの作成は「時間が掛かる処理」とされるため非同期にします。

プレビュー表示

●PdfPreviewウィジェット
作成したPDFをプレビュー表示させましょう。

PdfPreview(// プレビューの表示
  build: (format) async {
    final pdf = await makePdf();
    // .save()で「Uint8List」形式の作成
    return await pdf.save();
  },),
);  

PdfPreviewウィジェットでプレビューの表示が出来ます。
.save()メソッドで「Uint8List」形式にして返します。

●画面側
画面側の実装をしましょう。

void main() {
  final body = Center( // ボディー
    child: PdfPreview(// プレビューの表示
    build: (format) async {
      final pdf = await makePdf();
      // .save()で「Uint8List」形式の作成
      return await pdf.save();
    },),
  );  

  final sc = Scaffold(
    body: body, // ボディー        
  );

  final app = MaterialApp(home: sc);
  runApp(app);
}

main関数はこんな感じになります。

全体のコード

main.dart(コピペで使えます)

import 'package:flutter/material.dart';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';

void main() {
  final body = Center( // ボディー
    child: PdfPreview(// プレビューの表示
      build: (format) async {
        final pdf = await makePdf();
        // .save()で「Uint8List」形式の作成
        return await pdf.save();
      },),
  );  

  final sc = Scaffold(
    body: body, // ボディー        
  );

  final app = MaterialApp(home: sc);
  runApp(app);
}

// PDF作成
Future makePdf() async {
  final pdf = pw.Document();
  final page = pw.Page(
    build: (pw.Context context) {
      return pw.Center(
        child: pw.Text("PDF Test"),
      ); // Center
    }
  );

  pdf.addPage(page); 

  return pdf;
}

とにかく短さを意識し「40行未満」で書きました。
おそらく世界最短レベル。

動作確認

では、動作確認をして見ましょう

一見、真っ白だが、テキストはど真ん中に表示しているので

下にスクロールすると「PDF Test」が出てくる。

プレビューワーの機能許可

アンダーバーに以下の機能選択が付加されてます

1.「印刷」ボタン
2.「シェア」ボタン
3.「ページフォーマット」の選択
4.「向き(縦か横)」の切り替え
5.「デバッグ」機能の可否

順々に試して行きましょう

1.「印刷」ボタンで印刷画面表示。この「保存」ボタンで保存が出来る。

2.「シェア」ボタンですが、Webの場合は押した瞬間にPDFがダウンロードされます。

3.「ページフォーマット」の選択
A4かLetterの選択肢。

4.「向き(縦か横)」の切り替え
Webのためか、テキストがど真ん中に一行だけのためか、切り替えても余り変わらないような気もした

5.「デバッグ」機能の可否
こんな感じになる

●パラメーター調整
サジェストでPdfPreviewの引数を確認してみる。

まあ、これは一部ですがパラメーターの調整をしてみましょう。

印刷許可だけ残して見ると

「印刷」ボタンのみになる🖨

印刷許可も残さないようにすると

プレビューワーのみになる📄

それにしても、たったの40行のコードだけで、これだけの機能が実装できるとは …… 
Flutterのパッケージ恐るべしですね。

ただ、このままでは日本語を使うと文字化けします。
次項にて「日本語フォントに対応」して見ましょう。

日本語フォントに対応する

フォントをダウンロードする

●日本語フォント対応
日本語フォント対応は、2つの方法がある。

1.日本語フォントをダウンロードして、アセットに設置する
2.PdfGoogleFontsクラスによるフォント取得(Googleの用意したフォントに限られます)

本記事では「1」のアセットに設置する方法を説明する
(「2」に関しては技術書出版の予定)

●フォントファイルの種類
FlutterのPDF出力では「.ttfファイル」を使います。

●GoogleFonts公式
GoogleFonts公式(https://fonts.google.com)にアクセス

最初は、この「Noto Sans Japanese」が良さげに思ったのですが、FlutterのPDF出力で使うと何故か文字化けしてまう ……

●フォントのダウンロード
で、調べて見ると、Shippori Mincho(しっぽり明朝)が良さげなので、今回はこれを使います。

shippori で検索をかける

「Get Font」ボタン押下

「Download All」ボタン押下

すぐ左にある、このボタンからもダウンロード出来そう

ダウンロード完了したらディレクトリを開く

.zipファイルなので解凍する

太さの段階が一通りあるので

「Regular」を使いましょう。

注意点

【注意点】
フォントを使う際には
・有料か無料か?
・商用利用は可か?
など、規約を確認しておきましょう。
『しっぽり明朝』 については、「無料」かつ「商用利用可」とあります(※ SILオープンフォントライセンス)

アセットに設置する

●アセットに設置する
プロジェクト内にフォントファイルを置きます。
慣例的には

1.assetsフォルダを作成
2.その中にfontsフォルダを作成
3.その中にフォントファイルを置く

となるかと思います。

●アセットの定義
アセットに置いたフォントファイルを使うには「pubspec.yaml」にパスを定義する必要があります。

flutter:
  uses-material-design: true
  fonts:
    - family: Shippori
      fonts:
        - asset: assets/fonts/ShipporiMincho-Regular.ttf

pubspec.yaml」に追記
 

実装例 

●フォントの取得 コード内では、rootBundleを使いフォントファイルを取得します。
( servicesパッケージのインポートが必要です )

import 'package:flutter/services.dart'; // これを追記

・rootBundleを使い、アセットにあるフォントを取得する例

final fontData = await rootBundle.load('assets/fonts/ShipporiMincho-Regular.ttf');
final font = pw.Font.ttf(fontData);

・使用例

final page2 = pw.Page(
    pageTheme: pw.PageTheme( // ページのテーマ
      pageFormat: PdfPageFormat.a4, // ついでにA4にして見る
      theme: pw.ThemeData.withFont(base: font), // フォントを設定
    ),
    build: (pw.Context context) {
      return pw.Center(
        child: pw.Text("テキスト"),
      ); // Center
    }
  );

全体のコード

main.dart(コピペで使えます)

import 'package:flutter/material.dart';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';

import 'package:flutter/services.dart'; // rootBundleで使用

void main() {
  final body = Center( // ボディー
    child: PdfPreview(// プレビューの表示
      build: (format) async {
        final pdf = await makePdf();
        // .save()で「Uint8List」形式の作成
        return await pdf.save();
      },),
  );  

  final sc = Scaffold(
    body: body, // ボディー        
  );

  final app = MaterialApp(home: sc);
  runApp(app);
}

// PDF作成
Future makePdf() async {

  // フォントの読み込み
  final fontData = await rootBundle.load('assets/fonts/ShipporiMincho-Regular.ttf');
  final font = pw.Font.ttf(fontData);

  final pdf = pw.Document();
  
  final page = pw.Page(
    build: (pw.Context context) {
      return pw.Center(
        child: pw.Text("PDF Test"),
      ); // Center
    }
  );

  final page2 = pw.Page(
    pageTheme: pw.PageTheme( // ページのテーマ
      pageFormat: PdfPageFormat.a4, // ついでにA4にして見る
      theme: pw.ThemeData.withFont(base: font), // フォントを設定
    ),
    build: (pw.Context context) {
      return pw.Center(
        child: pw.Text("テキスト"),
      ); // Center
    }
  ); 

  pdf.addPage(page); 
  pdf.addPage(page2); 

  return pdf;
}

●動作確認

動作確認しましょう。

まずは先ほどの1ページ目です

日本語を使っている2ページ目です。
文字化けせず表示されているのが分かります。

「Pixel Fold」での動作確認

Web以外でもテストして見ましょう。

●Android

Androidのエミュレーター(Pixel Fold)を使います

やはりM3のMacbookAirだとサクサク動いて快適ですね✨

1ページ目の真ん中

2ページ目の真ん中

「印刷」ボタン押下

いいですね〜🖨

左上のボタン押下で、表示されるメニュー

「シェア」ボタンも押しちゃうぞ💡

このように色々選べる

●iOS
iOSの方もテストしたいと所ですね。
先週辺りに「Xcode16」がリリースされたので、それをFlutterと連携させてテストする予定です。

Xcode16」導入の記事にて記載します📝

Flutter技術書の執筆予定

なお、Flutterの技術書(電子書籍)を執筆する予定です。

・表形式での表示(一覧など)
・PdfGoogleFontsクラスによるフォント取得(アセット設置の手間が無い)
・印刷ボタンの単体処理(プレビュー無し)
・PDFのマージン
・PDFの向き
・最大ページの調整
・ローディングバーの調整
・メタデーター(author: や creator: など)の指定
・MultiPageウィジェットによる複数ページに跨いでの表示

など、さらに凝った内容を書くので、ご興味のある方は是非ご購読頂ければと思います。

早ければ、2024年10月半ばより着手。
2025年の年明け辺りに出版出来ればなと考えております。

著書

【辛島信芳の著書】
IT技術などに興味のある方は、是非ご覧になってください。


この記事が気に入ったらサポートをしてみませんか?