ZF3でTCPDFを使って任意のテキストを入れたPDFを出力する

※本稿では、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サイズ:縦)を用意します。
各ページの異なるところはページ番号だけです。

これを例えば、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. 元々 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;
          }
      // ~~省略~~
    • emptyPJ/module/Mymodule/view/mymodule/index/print-pdf.phtml
      <?php
          if (isset($this->pdf)) {
              $this->pdf->Output("output.pdf", 'I');
          } else {
              echo 'Error';
          }
      
  3. テンプレート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によって追加されたテキストの色を赤にしたのは、わかりやすさのためです。
テンプレートPDFに当て込む書式は様々あると思いますが、
PdfManagerクラスの create()メソッド以降の処理を適宜書き換えることで、
好きなようにPDFに文字を書き込んでいくことが出来るはずです。



今回の記事は以上です。

コメント

このブログの人気の投稿

LaravelのTestCaseを継承したテストにて use RefreshDatabase を使って artisan db:seed --class='TestSeeder' まで実行させるやり方

ZF3で空のプロジェクトを作成する自分なりの手順

Androidでical4jを使う