👻

【過去Blogからの移行記事】ZF3でTCPDFを使って任意のテキストを入れたPDFを出力する

2022/09/18に公開

※本稿では、Zendframework 3 のことを ZF3 とか zf3 と呼んでいます。
※2017年6月18日 2:13AM に投稿した本稿(当初postId=4172088349873447690)を、 少し修正しようとして操作を誤り、翌日1:15AM頃に削除してしまいました。
※当初の記事内容に若干の修正を加えて再アップします。(提示したPdfManagerクラスの汎用性を少し改善した)

空のZF3プロジェクトへTCPDFを導入

php で TCPDF を使いたい場合、 composer を活用すれば簡単に導入ができます。

参考記事: TCPDF + FPDIで既存PDFに日本語文章を追加

上記リンク先を参考にしつつ、

仮に先日書いた記事 ZF3で空のプロジェクトを作成する自分なりの手順 で作った空のプロジェクト「emptyPJ」へ composer で組み込む、とした場合は次のようにします。

1. composer.json に下記を追記

composer.jsonの中身

{
    "require": {
        "php": "^5.6 || ^7.0",
        "zendframework/zend-component-installer": "^1.0",
        "zendframework/zend-mvc": "^3.1", // ←行末にコロン
        "tecnickcom/tcpdf": "6.2.*",      // ←追記
        "setasign/fpdi": "1.6.*",         // ←追記
        "setasign/fpdi-tcpdf": "1.6.*"    // ←追記
    },
}

2. composer update する

Windows上 Git Bash でのコマンド実行例

hoge@host MINGW64 ~/work/emptyPj
$ php composer.phar update
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 3 installs, 0 updates, 0 removals
  - Installing setasign/fpdi (1.6.2): Loading from cache
  - Installing tecnickcom/tcpdf (6.2.12): Loading from cache
  - Installing setasign/fpdi-tcpdf (1.6.1): Loading from cache
setasign/fpdi suggests installing setasign/fpdf (FPDI will extend this class but as it is also possible to use "tecnickcom/tcpdf" as a
n alternative there's no fixed dependency configured.)
setasign/fpdi suggests installing setasign/fpdi-fpdf (Use this package to automatically evaluate dependencies to FPDF.)
Writing lock file
Generating autoload files

3. ひとまず IndexController の indexAction で new FPDI() してみる。

※ FPDIクラスは、TCPDFクラスを継承したもの。(上述のcomposer update でインストールされたもの達によって下準備済み)

emptyPJ/module/Mymodule/src/Mymodule/Controller/IndexController.php

// ~~省略~~
    public function indexAction()
    {
        $pdf = new \FPDI();
        return new ViewModel(array('version'=>$pdf->getPdfVersion()));
    }
// ~~省略~~

emptyPJ/module/Mymodule/view/mymodule/index/index.phtml

<?php
    echo 'index/index: PDF version:'.$this->version;

4. 当該ページへアクセス

「index/index: PDF version:1.3」のような表示になったらひとまず動作できている。

以後、上述した emptyPJ を編集していく形で説明を続けます。

※PDF出力の実装は別のアクションで行うので、
※上記まででindexAction() や index.phtml へ加えた変更は破棄してください。

既存のPDFテンプレートに任意のテキストを入れて出力する

背景にセットするPDFテンプレートを用意する

ひとまず、次のような2ページに渡るPDFテンプレート(A4サイズ:縦)を用意します。
各ページの異なるところはページ番号だけです。

dummy-2.pngdummy-1.png

これを例えば、emptyPJ/data/pdf_tamplate/ 配下に sample.pdf というファイル名で配置します。
※pdf_tamplateディレクトリは予め作成してください。

PDFファイルにあれこれ手を加えるModelクラスを用意する

次に、PDFファイルを好きに扱うためのModelクラスを作ります。
※Modelディレクトリは予め作成してください。

1.  emptyPJ/module/Mymodule/src/Mymodule/Model/MyPdf.php

    <?php
    namespace Mymodule\Model;

    /**
     * PDFクラス
     * @see TCPDF
     */
    class MyPDF extends \FPDI
    {
        /** 日本語フォント */
        const FONT_JP = 'kozminproregular';

        /**
         * ヘッダー
         * @see TCPDF::Header()
         */
        public function Header()
        {
            $this->SetFont(self::FONT_JP, '', 8);
            $outputDate = 'date: '.date('Y-n-j');
            $this->Cell(0, 8, $outputDate, 0, 0, 'R');
        }

        /**
         * フッター
         * @see TCPDF::Footer()
         */
        public function Footer()
        {
            $this->Cell(0, 8, $this->getAliasNumPage() . '/' . $this->getAliasNbPages(), 0, 0, 'C');
        }

    }

2.  emptyPJ/module/Mymodule/src/Mymodule/Model/PdfManager.php

    <?php
    namespace Mymodule\Model;

    use Mymodule\Model\MyPdf;

    class PdfManager
    {
        const DEFAULT_PDF_TMPL_PATH = '/data/pdf_template/dummy.pdf';

        /** @var string */
        const ORIENTATION_PORTRAIT = 'P';
        /** @var string */
        const ORIENTATION_LANDSCAPE = 'L';
        /**
         * 用紙の向き
         *     縦(デフォルト)
         *     横
         * @var array
         */
        const ORIENTATION_ARRAY = array(
            self::ORIENTATION_PORTRAIT,
            self::ORIENTATION_LANDSCAPE
        );

        /** @var string */
        const PAGE_FORMAT_A4 = 'A4';
        /** @var string */
        const PAGE_FORMAT_A3 = 'A3';
        /**
         * 用紙フォーマット
         *     A4: 210x297 mm (デフォルト)
         *     A3: 297x420 mm
         * @var array
         */
        const PAGE_FORMAT_ARRAY = array(
            self::PAGE_FORMAT_A4,
            self::PAGE_FORMAT_A3
        );

        /** @var string */
        const ENCODING_UTF8 = 'UTF-8';


        /** @var TCPDF */
        protected $pdfObj;

        /** @ver string */
        protected $templatePath;

        /**
         * @param string $pageOrientation default: ORIENTATION_PORTRAIT
         * @param string $pageFormat
         * @param string $encode
         * @param string $tmplPath
         */
        public function __construct(
                $pageOrientation = self::ORIENTATION_PORTRAIT,
                $pageFormat = self::PAGE_FORMAT_A4,
                $encode = self::ENCODING_UTF8,
                $tmplPath = null    )
        {
            $pageOrientation = in_array($pageOrientation, self::ORIENTATION_ARRAY)
                            ? $pageOrientation : self::ORIENTATION_PORTRAIT;
            $pageFormat = in_array($pageFormat, self::PAGE_FORMAT_ARRAY)
                            ? $pageFormat : self::PAGE_FORMAT_A4;
            $encode = empty($encode)
                            ? self::ENCODING_UTF8 : $encode;

            if ( ! (isset($this->templatePath) && is_null($tmplPath)) ) {
                $this->templatePath = is_null($tmplPath)
                            ? getcwd().self::DEFAULT_PDF_TMPL_PATH : $tmplPath;
            }

            $this->pdfObj = new MyPDF(
                    $pageOrientation, PDF_UNIT, $pageFormat, true, $encode, false
                );
            $this->pdfObj->SetFont(MyPDF::FONT_JP, '', 8);
            $this->pdfObj->setFontSubsetting(true);

        }

        /**
         * @return object TCPDF
         */
        public function getPdfObj()
        {
            return $this->pdfObj;
        }

        /**
         * 出力するPDFデータを作成する
         */
        public function create()
        {
            // 背景に既存のPDFを使うのでマージンを無効にする
            $this->pdfObj->SetAutoPageBreak(false, 0);
            $this->pdfObj->SetMargins(0, 0, 0);

            $this->pdfObj->setPrintHeader(false);
            $this->pdfObj->setPrintFooter(false);
            $this->pdfObj->AddPage();

            // 背景PDFセット
            $this->pdfObj->setSourceFile($this->templatePath);
            // 背景PDFページ設定
            $this->pdfObj->useTemplate($this->pdfObj->importPage(1));
        }

    }

ここで書いたcreate()メソッドがPDFの中身を作り込んでいく処理になりますが、この時点ではテンプレートPDFを背景にセットするのみです。後ほど、create()メソッドの中身にテキスト追加処理を書いていきます。

PDFを出力するためのアクションを用意する

次に、PDFを出力するための新しいアクションを IndexController に作成します。

1. IndexController

元々 emptyPJ の Mymodule では極力シンプルな作りにする目的でルーティングを固定してしまっているので、IndexController で複数のリクエスト先アクションを動的に扱えるよう、routes の type を Literal から Segment に変更します。

  • 編集対象: emptyPJ/module/Mymodule/config/module.config.php

2. 新しく追加するアクション名を printPdfAction() とします

  • 追加する場所: emptyPJ/module/Mymodule/src/Mymodule/Controller/IndexController.php
// ~~省略~~
use Mymodule\Model\PdfManager;    // ←追加

class IndexController extends AbstractActionController
{
// ~~省略~~
    public function printPdfAction()    // ←この関数ブロックを追加
    {
        $pdfMgr = new PdfManager();

        // PDFデータを作成する
        $pdfMgr->create();

        // view にPDFをアタッチする
        $view = new ViewModel(array('pdf' => $pdfMgr->getPdfObj()));
        $view->setTerminal(true);
        return $view;
    }
// ~~省略~~

3. phtml

  • 配置先: emptyPJ/module/Mymodule/view/mymodule/index/print-pdf.phtml
<?php
    if (isset($this->pdf)) {
        $this->pdf->Output("output.pdf", 'I');
    } else {
        echo 'Error';
    }

テンプレートPDFの1ページ目のみの出力が得られれば、新しく作成したModelクラスがひとまず動作できているということです。

PDFファイルにあれこれ手を加えてみる

最後に、先ほど作成した ModelクラスでPDFに対してあれこれ編集して出力を見てみます。

emptyPJ/module/Mymodule/src/Mymodule/Model/PdfManager.phpを次のように編集

// ~~省略~~

// 定数・変数の定義領域に下記を追加定義

    // ページ内で行を順次処理する場合: テキスト描画時に行処理を始めるカーソル位置
    const ROW_START_X = 20;
    const ROW_START_Y = 28;
    const LINE_HEIGHT = 6.5;

    // ページ内で行を順次処理する場合: 現在のカーソル位置
    protected $curX = 0;
    protected $curY = 0;

// ~~省略~~

// create()メソッド部分を変更するとともに処理の追加を施す

    /**
     * 出力するPDFデータを作成する
     */
    public function create()
    {
        if (!empty($this->templatePath)) {
            /*
             * 背景PDFセット
             */
            if (file_exists($this->templatePath)) {
                $this->pdfObj->setSourceFile($this->templatePath);
            } else {
                throw new \Exception('The specified PDF template file does not exist.');
            }

            // 背景に既存のPDFを使うのでマージンを無効にする
            $this->pdfObj->SetAutoPageBreak(false, 0);
            $this->pdfObj->SetMargins(0, 0, 0);
            // 上記と同様の理由で、ヘッダーやフッターを差し込まない
            $this->pdfObj->setPrintHeader(false);
            $this->pdfObj->setPrintFooter(false);
        }

        // テキスト色を赤にセット
        $this->pdfObj->SetTextColor(255, 0, 0);

        // コンテンツを作成
        $this->makeContents();
    }

    /**
     * PDFに出力するコンテンツを作成する
     */
    protected function makeContents()
    {
        // ページごとにコンテンツを生成
        $this->add1stPage();
        $this->add2ndPage();
    }

    /** 1ページめのコンテンツを生成 */
    public function add1stPage()
    {
        $this->pdfObj->AddPage();

        // create()で setSourceFile()が行われていればページ背景をセット
        if (!is_null($this->pdfObj->currentFilename)) {
            //背景PDFページ設定
            $this->pdfObj->useTemplate($this->pdfObj->importPage(1));
        }

        // 表のセルを埋めるように表示
        $this->curY = self::ROW_START_Y;
        $this->rowProcessing("報告書A", "鈴木", "", "", false, false);
        $this->rowProcessing("報告書B", "塚本", "", "", false, false);
        $this->rowProcessing("報告書C", "藤田", "", "", false, false);
        $this->rowProcessing("報告書D", "浜野", "", "", false, false);

        // テキトーな文言をテキトーな位置に表示
        $this->pdfObj->Text(50, 100, "ポケモンゲットだぜ!!");
    }

    /** 2ページめのコンテンツを生成 */
    public function add2ndPage()
    {
        $this->pdfObj->AddPage();

        // create()で setSourceFile()が行われていればページ背景をセット
        if (!is_null($this->pdfObj->currentFilename)) {
            //背景PDFページ設定
            $this->pdfObj->useTemplate($this->pdfObj->importPage(2));
        }

        // テキトーな文言をテキトーな位置に表示
        $this->pdfObj->Text(50, 100, "スライムナイトは仲間になりたそうにこちらを見ている");

        // 表のセルを埋めるように表示
        $this->curY = self::ROW_START_Y;
        $this->rowProcessing("鬼平犯科帳", "池波", "", "", false, false);
        $this->rowProcessing("修造語録", "松岡", "", "", false, false);
    }

    /**
     * 表に対して1行ずつのテキスト表示処理
     */
    public function rowProcessing($docName, $assign, $expTime, $startDate, $stMake = false, $stSend = false)
    {
        $this->curX = self::ROW_START_X;
        $this->curY += self::LINE_HEIGHT;
        // 資料名
        $this->pdfObj->Text($this->curX, $this->curY, $docName);
        // 担当
        $this->curX += 60;
        $this->pdfObj->Text($this->curX, $this->curY, $assign);
        // 期限
            // ~~処理~~
        // 着手日
            // ~~処理~~
        // 作業完了
            // ~~処理~~
        // 提出済み
            // ~~処理~~
    }

以上によって、printPdfActionへアクセスした時に得られるPDFの中身は次のようになりました。
※TCPDFによって追加されたテキストの色を赤にしたのは、わかりやすさのためです。

output-1.pngoutput-2.png

テンプレートPDFに当て込む書式は様々あると思いますが、
PdfManagerクラスの create()メソッド以降の処理を適宜書き換えることで、
好きなようにPDFに文字を書き込んでいくことが出来るはずです。

今回の記事は以上です。

Discussion