座標を使ってPDFを出力するTCPDFの使い方

# 概要

[EC-CUBE4のプラグインを開発する際](https://qiita.com/haruna-nagayoshi/items/948b196cf4f5186641e9)、帳票をPDFで出力する機能を作り、TCPDFというライブラリで座標を使って描画する方法を学んだのでまとめました。

この記事を読めば線、表、画像の描画といったひととおりの実装はできる、というレベルを目指して丁寧に書きました。
**使い方だけを知りたい人は「基本」「応用」の項を読んでください。**
※EC-CUBE4で標準機能として導入されているライブラリのため、composerで新たに追加する方法は本記事では説明しません。

TCPDFはHTMLで描画することも一応可能ですが、座標を使って描画することに慣れたほうが楽だと今は考えています。

# 動作環境

```
EC-CUBE 4.0.3
Symfony 3.4
PHP 7.3
TCPDF 6.2.26
```

# TCPDFについて

「PDF PHP」で検索すると真っ先に検索結果としてでてくるくらいにはメジャーらしい。(他のPDF関連のライブラリを使ったことがないため比較できない。)

EC-CUBE4では、TCPDFを使ってPDFを出力しているが、TCPDFに関してネットで得られる情報は古いものが多いため、自分で試行錯誤する必要があった。

## TCPDFはHTMLでPDF生成が可能とされている

HTMLでテンプレートを作成できる...とはいうものの使えるCSSに制限がある。floatは使えないらしいので、tableタグでうまいことパーツを並べる必要がある。

-  [HTMLからPDFを出力できるPHPライブラリ「TCPDF」](https://fivestar-corporation.co.jp/archives/12532)
   - [上記サイトのPDFサンプル](https://fivestar-corporation.co.jp/wp-content/uploads/2019/09/test.pdf)
-  [HTMLをそのままPDFにできるPHPライブラリ TCPDF](http://bashalog.c-brains.jp/17/11/16-100606.php)

こちらはLaravel+TCPDFでbladeをPDFテンプレートにした例。ここでもtableタグで描画している。

- [STEP09：Laravel5.7 でデータを埋め込んだPDFを生成する](https://qiita.com/nobu-maple/items/d60189231e9db9cb6b5a)

## PDFファイルをテンプレートにすることもできる

領収書のようにある程度文字数が予想できて、表示位置が固定なら、PDFでテンプレートを用意してテキスト出力位置を座標指定することもできる。(注文明細のように購入数によって出力する行数が変動する場合、この方法は使えない)
その場合は、EC-CUBE4のソースや下記の記事が参考になると思う。

- [PHPでPDF帳票を出力する方法（TCPDF + FPDI）](https://qiita.com/emegane/items/486975a79ebb267c2b8e)
- [TCPDFを使ってPHPでPDFに文字を出力したい](https://qiita.com/panax/items/7bb808faeef2c5a58b7f)

ただし、普段Adobe Acrobat DCなどでPDFを作成することに慣れていない場合、非常に時間がかかると思う。PDFやExcel(※)でテンプレートとなる資料を提供されているなら、この方法も有力だと思う。

※Excelで印刷範囲として設定した範囲をPDFとして出力し、テンプレートとして使う。出力したあとにセンタリングなどレイアウトを調整したい場合はAdobe Acrobat DCを使うと楽そう。(Adobe Acrobat DCは少しだけ触った)

## twigを使ってPDFを出力しようとしたができなかった

HTMLでPDF出力できるライブラリなので、symfonyでもrender()メソッドを使えばtwigでPDFを生成できるのでは...！？と挑んだが、できなかった。
いろいろ試したものの、以下の問題が解決せず、丸一日使っても情報を得られなかったのでいちから座標でPDFを出力する方針に切り替えた。

- 下線がCSSで引けたり引けなかったりする
  - 帳票タイトル、社名、宛名、に下線を引きたいのに引けない。
  - しかも、引ける/引けないの基準が分からない

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/c7cfdfb1-ce6a-b04e-2027-f9baaeddbcd1.png)

- ↓のように$orderItems変数をtwigで受け取ってfor文使って出力しようとすると、何も出力されない
  - しかし、for i in 0..3 みたいなfor文は出力される

```twig
    {% for orderItem in orderItems %}
        <table class="tbl">
            <tr>
                <th>品名</th>
                <th>数量</th>
                <th>単価</th>
                <th>金額</th>
            </tr>
            <tr>
                <td>{{ orderItem.name }}</td>
                <td>{{ orderItem.quantity }}</td>
                <td>{{ orderItem.price }}</td>
                <td>{{ orderItem.totalPrice }}</td>
            </tr>
        </table>
    {% endfor %}
```

# 基本

この項では実際に使ったことのあるメソッドについて説明する。(使ったことのない変数やオプションもある)

こちらのサイトの表を引用しつつ、翻訳されていない部分などに手を加えた。
[TCPDFマニュアル (勝手訳)](http://tcpdf.penlabo.net)

## 用紙サイズ

**縦向きA4用紙のサイズは横210mm × 縦297mm**
x,y座標で出力位置を決めるので、横幅の半分はx = 105mm、横の1/4は大体50mm、などと覚えておくと楽になる。
他の用紙サイズはこちら  [Joshin 用紙サイズ早見表](https://joshinweb.jp/supply/paper_size.html)

## __construct()

**__construct**(\$orientation='P', \$unit='mm', \$format='A4', \$unicode=true, \$encoding='UTF-8', \$diskcache=false, \$pdfa=false)
全て初期値の状態で実装したので、初期値以外の挙動は把握していない。

| 型     | 変数名       | 初期値  | 説明                                                         |
| :----- | ------------ | ------- | ------------------------------------------------------------ |
| float  | $orientation | 'P'     | 用紙の向き<br />設定可能な値は以下（大文字と小文字を区別しない）<br />P：縦（デフォルト）<br />L：横<br /> ''（空の文字列）：自動方向付け |
| float  | $unit        | 'mm'    | 測定単位<br />pt：point<br /> mm：ミリメートル（デフォルト）<br /> cm：センチメートル<br /> in：インチ <br /><br />ポイントは1/72インチ、つまり約0.35 mm（1インチは2.54 cm）。これはタイポグラフィでは非常に一般的な単位。フォントサイズはその単位で表される。 |
| string | $format      | 'A4'    | 用紙サイズ<br />getPageSizeFromFormat（）で指定された文字列値の1つ、またはsetPageFormat（）で指定されたパラメーターの配列のいずれか。 |
| int    | $unicode     | true    | trueの場合、入力テキストがユニコードであることを意味する。   |
| string | $encoding    | 'UTF-8' | 文字セットエンコーディング（htmlエンティティを元に戻すときにのみ使用される) |
| bool   | $diskcache   | false   | 非推奨の機能                                                 |
| bool   | $pdfa        | false   | trueの場合、ドキュメントをPDF / Aモードに設定                |

## 新規ページを作成する
**addPage**(\$orientation, \$format)
ページを追加する。

[TCPDFマニュアル (勝手訳) AddPage](http://tcpdf.penlabo.net/method/a/AddPage.html)

> 2ページ目以降を追加する場合は、[Footer()](http://tcpdf.penlabo.net/method/f/Footer.html)関数が呼び出され、処理中のページにフッターが追加される。
> ページ追加後カーソルはページの左上に移動し、[Header()](http://tcpdf.penlabo.net/method/h/Header.html)関数が呼び出されページヘッダーを追加される。

| 型     | 変数名       | 初期値 | 説明                                                         |
| :----- | ------------ | ------ | ------------------------------------------------------------ |
| string | $orientation | ''     | 用紙の向き<br />P or PORTRAIT（縦：既定）<br />L or LANDSCAPE（横） |
| mixed  | $format      | ''     | 用紙の種類。以下のいずれか。<br />4A0、2A0、A0、A1、A2、A3、A4（既定）、A5、A6、A7、A8、A9、A10、B0、B1、B2、B3、B4、B5、B6、B7、B8、B9、B10、C0、C1、C2、C3、C4、C5、C6、C7、C8、C9、C10、RA0、RA2、RA3、RA4、SRA0、SRA1、SRA2、SRA3、SRA4、LETTER、LEGAL、EXECUTIVE、FOLIO<br /><br />カスタムページサイズの場合はheightとwidthの配列を指定。 |
| bool   | $keepmargins | false  | trueの場合、新しいページの余白に現在のページの余白の幅が引き継がれる。 |
| bool   | $tocpage     | false  | trueの場合、tocpageの状態をtrueに設定します（追加されたページは目次の表示に使用されます）。<br /><br />tocpageが何のページなのか分からないが、恐らく目次として定義するかどうか…だと思う。 |

## ヘッダーを出力する
**setPrintHeader**($val)
true(初期値)を指定すればページヘッダーが出力され、falseを指定すればヘッダーは出力されない。

## フッターを出力する
**setPrintFooter**($val)
ヘッダーと同じく、true(初期値)を指定すればページフッターが出力され、falseを指定すれば出力されない。

## 線を書く
**line**(\$x1, \$y1, \$x2, \$y2, \$style)

| 型    | 変数名 | 初期値      |                                                  |
| :---- | ------ | ----------- | ------------------------------------------------ |
| float | $x1    |             | 開始点のX座標                                    |
| float | $y1    |             | 開始点のY座標                                    |
| float | $x2    |             | 終了点のX座標                                    |
| float | $y2    |             | 終了点のY座標                                    |
| array | $style | [] (空配列) | 線種<br />setLineStyle()のような配列を指定する。 |

### 線種
線種の配列は、以下のアイテムをキーとする連想配列を指定するらしい。
[[TCPDFマニュアル (勝手訳) SetLineStyle](http://tcpdf.penlabo.net/method/s/SetLineStyle.html)

>- width (float): 線の幅
>- cap (string): 線の末端部のスタイル、(butt, round, squareのいずれか).
>- join (string): 線の結合部のスタイル、(miter, round, bevelのいずれか)。
>- dash (mixed): 破線パターン on-offの組み合わせ。
>  例えば、'2'とすると長さ2のonに長さ2のoffを繰り返す。
>  また、'2,1'とすると長さ2のonの後に長さ1のoffを繰り返す。
>- phase (integer): 破線パターンの開始位置のシフトする長さ.
>- color (array): 前景色、array(GREY)もしくはarray(R,G,B)もしくはarray(C,M,Y,K)。.

## 文字を出力する
**text**(\$x, \$y, \$txt, \$fstroke, \$fclip, \$ffill, \$border, \$ln, \$align, \$fill, \$link, \$strech, \$ignore_min_height, \$calign, \$valign, \$rtloff )

出力開始位置を座標(x,y)で指定し、出力したい文字列を渡す。

| 型     | 変数名             | 初期値 | 説明                                                         |
| :----- | ------------------ | ------ | ------------------------------------------------------------ |
| float  | $x                 |        | 出力開始位置のX座標                                          |
| float  | $y                 |        | 出力開始位置のY座標                                          |
| string | $txt               |        | 表示する文字列                                               |
| int    | $fstroke           | false  | フォント・アウトラインの太さ (falseであれば、アウトラインなし)<br />※次の項を参照 |
| bool   | $fclip             | false  | trueであれば、クリッピングモードになる。 クリッピングする場合、[StartTransform()](http://tcpdf.penlabo.net/method/s/StartTransform.html)を実行して変換開始を宣言してからText()を使用する。 変換処理が終了する際には[StopTransform()](http://tcpdf.penlabo.net/method/s/StopTransform.html)を呼び出して、終了を宣言する必要がある。<br />※利用しなかったため詳細は不明 |
| bool   | $ffill             | true   | trueであれば、文字を塗る                                     |
| mixed  | $border            | 0      | セルの罫線を描画するかどうか、描画するなら上下左右どれを描画するかを指定できる。 <br />次のいずれかの値を指定する。<br />0：境界なし（デフォルト）<br />1：フレームまたは次の文字の一部またはすべてを含む文字列（任意の順序）：<br />L：左<br />T：上部<br />R：右<br />B：下部<br />または各境界の線スタイルの配列：<br />例：array（ 'LTRB' => array（ 'width' => 2、 'cap' => 'butt'、 'join' => 'miter'、 'dash' => 0、 'color' = > array（0、0、0）））<br />※配列で指定した例はPHPDocにあるが、利用しなかったため未検証。<br /> |
| int    | $ln                | 0      | テキスト出力後の位置を示す。<br />次のなかから指定する。<br />0：右（またはRTL言語の場合は左）。これが初期値。<br />1：次の行の先頭<br />2：belowPutting 1は0を入れて直後にLn（）を呼び出すのと同じです。<br /><br />text()の$lnは利用しなかったため未検証。テキスト出力後、そのまま続けて出力したければ0、改行して次のテキストを出力したければ1を指定するのだと思う。 |
| string | $align             | ''     | 水平方向の文字揃えを指定する。<br />Lまたは空の文字列：左揃え（デフォルト値）<br />C：中央<br />R：右揃え<br />J：両端揃え<br /><br />Jの挙動がいまいちわからなかった・・・。 |
| bool   | $fill              | false  | trueであれば、背景を塗る。 falseであれば、透過する。<br /><br />※利用しなかったため未検証 |
| mixed  | $link              | ''     | テキストにリンクを貼る。<br />※利用しなかったため未検証      |
| int    | $strech            | 0      | フォントストレッチモード<br />0 =無効<br />1 =テキストがセル幅よりも大きい場合にのみ水平スケーリング<br />2 =セル幅に合わせて強制的に水平スケーリング<br />3 =テキストがセル幅よりも大きい場合にのみスケーリング<br />4 =セル幅に合わせて文字の間隔を強制する。一般的なフォントのストレッチとスケーリングの値は、可能な場合は保持されます。<br />※利用しなかったため未検証<br />セル幅に対して文字列が少ないときに文字間隔を広げたり、反対にセル幅に対して文字列が多いときに間隔を狭めたり文字自体を小さくしたりするのだと思う。 |
| bool   | $ignore_min_height | false  | trueなら高さの最小値を自動的に補正する<br />※利用しなかったため未検証<br />これもよくわからない・・・。 |
| string | $calign            | 'T'    | 指定したY値に対するセルの垂直方向の配置。<br /><br />T：セルトップ<br/>A：フォントトップ<br/>L：フォントのベースライン<br/>D：フォント下部<br/>B：セル底 |
| string | $valign            | 'M'    | 垂直方向の文字揃えを指定する。<br />T：上<br/>C：中央<br/>B：下 |
| bool   | $rtloff            | false  | trueなら、$ xと$ yの初期位置の軸の原点としてページの左上隅を使用します。<br /><br />※利用しなかったため未検証<br />これも訳しても意味が通らない・・・。 |

### $fstrokeを指定するとバグが発生する
\$fstrokeの用途がわからなかったため試しにtrueにしたらこうなった。正確な使い方はわからないままだが、初期値のfalseのままにしておいたほうが良いと思う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/4947c981-6d92-2f42-f037-bc52f316c793.png)

## フォントとフォントサイズを指定する
**SetFont**(\$family, \$style, \$size, \$fontfile, \$subset, \$out)
日本語フォントとフォントサイズを指定するために使った。

| 型     | 変数名    | 初期値    |                                                              |
| :----- | --------- | --------- | ------------------------------------------------------------ |
| string | $family   |           | [AddFont()](http://tcpdf.penlabo.net/method/a/AddFont.html)で追加したフォント名もしくは以下の標準フォントを設定する。<br />times (Times-Roman)<br />timesb (Times-Bol)<br />timesi (Times-Italic)<br />timesbi (Times-BoldItalic)<br />helvetica (Helvetica)<br />helveticab (Helvetica-Bold)<br />helveticai (Helvetica-Oblique)<br />helveticabi (Helvetica-BoldOblique)<br />courier (Courier)<br />courierb (Courier-Bold)<br />courieri (Courier-Oblique)<br />courierbi (Courier-BoldOblique)<br />symbol (Symbol)<br />zapfdingbats (ZapfDingbats)<br />空文字を指定するとこれまで使用していたフォントが使われる。<br /><br />PHPDocにはこのように書かれているが、実際には日本語フォントもある・・・のでこの一覧にないフォントもある模様。 |
| string | $style    | ''        | フォントスタイル<br />空文字: regular(デフォルト値)<br />B: ボールド<br />I: イタリック<br />U: アンダーライン<br />D: 取り消し<br />もしくは、上記の組み合わせ。<br /><br />また'symbol'か'zapfdingbats'フォントを選択した場合、ボールドとイタリックは無効。 |
| float  | $size     | null      | フォントサイズをポイントを指定する。<br />省略時は現在のフォントサイズとなり、<br />一度もフォントサイズが指定されていなければ12ptとなる。 |
| string | $fontfile | ''        | フォントファイルを名前で指定する。 フォント名はフォントファミリーとスタイルが定義されている必要がある。スペースなしの 小文字で指定する。<br /><br />※利用しなかったため未検証 |
| mixed  | $subset   | 'default' | trueの場合、使用した文字のみを埋め込む。(使用されている文字に関連する情報のみが格納される) <br />falseの場合、フォントを全て埋め込む。 <br />'default'の場合、setFontSubsetting()の設定に従う。 このオプションは、TrueTypeUnicodeフォントでのみ有効。 ユーザーがドキュメントを変更できるようにする場合は、このパラメータはfalseを指定すること。 フォントを埋め込まない場合、このPDFを受け取る側も同じフォントを持っている必要がある。 埋め込んだフォントのサイズだけ、PDFファイルのサイズも増加する。<br /><br />※利用しなかったため未検証。自分で追加したフォントを使用した場合にPDFにフォントのデータを埋め込むかどうか・・・という話をしているのだと思う。 |
| bool   | $out      | true      | trueの場合はフォントサイズコマンドを出力し、それ以外の場合はフォントプロパティのみを設定します。 |

### 日本語を出力する場合
ゴシック体は`kozgopromedium`、明朝体は`kozminproregular`を指定すればOK。
(しっかり見比べたわけではないが、どちらも似たようなフォントに感じる・・・)

```php
$this->tcpdf->setFont('kozgopromedium', '', 10);
```

## フォントサイズを変える
**setFontSize**(\$size, \$out)

| 型    | 変数名 | 初期値 | 説明                                                         |
| :---- | ------ | ------ | ------------------------------------------------------------ |
| float | $size  |        | フォントサイズ(ポイント)                                     |
| bool  | $out   | true   | trueであれば、フォントサイズコマンドを出力する。 でなければ、フォントのプロパティを使用する。 |


テキストを出力する前にsetFontSize()を呼ぶことでフォントサイズを変えられる。

```php
$this->tcpdf->setFontSize(20);
$this->tcpdf->text(10, 10, 'こんにちは');  // ←20pt
$this->tcpdf->setFontSize(10);
$this->tcpdf->text(10, 20, 'こんにちは');  // ←10pt
```

## 表を描画する
**multiCell**(\$w, \$h, \$txt, \$border, \$align, \$fill, \$ln, \$x, \$y, \$reseth, \$stretch, \$ishtml, \$autopadding, \$maxh, \$valign, \$fitcell)
罫線付きでセル（矩形領域）を表示する。
セルを描画後のカーソル位置は、右または次の行を指定できる。

| 型   | 変数名 | 初期値       | 説明  |                                                              |
| :--- | ------ | ------------ | ----- | ------------------------------------------------------------ |
| 1    | float  | $w           |       | セル幅、0とすると右端まで。                                  |
| 2    | float  | $h           |       | セルの最小の高さ。 セル幅に対して文字が収まるかどうかで、高さが自動的に拡張される。隣のセルと高さを合わせるにはgetLastH()を渡す。※詳細は後述    |
| 3    | float  | $txt         |       | 文字列                                                       |
| 4    | float  | $border      | 0     | 境界線の描画方法を以下のいずれかの値で指定する。<br />0: 境界線なし(既定)<br />1: 枠で囲むまたは、以下の組み合わせで境界線を指定する<br />L: 左<br />T: 上<br />R: 右<br />B: 下 |
| 5    | string | $align       | 'J'   | テキストの整列を以下のいずれかで指定する<br />L or 空文字: 左揃え<br />C: 中央揃え<br />R: 右揃え<br />J: 両端揃え ($ishtml=falseの場合の既定値) |
| 6    | bool   | $fill        | false | 背景の塗つぶし指定 [0:透明(既定) 1:塗つぶす]                 |
| 7    | int    | $ln          | 1     | 出力後のカーソルの移動方法を指定する0: 右へ移動(既定)、但しRTL言語の場合は左へ移動1: 次の行へ移動2: 下へ移動 |
| 8    | float  | $x           | ''    | X座標(省略時は現在位置)                                      |
| 9    | float  | $y           | ''    | Y座標(省略時は現在位置)                                      |
| 10   | bool   | $reseth      | true  | 前回のセルの高さ設定をリセットする場合はtrue、引き継ぐ場合はfalse |
| 11   | int    | $stretch     | 0     | テキストの伸縮(ストレッチ)モード<br />0 = なし<br />1 = 必要に応じて水平伸縮<br />2 = 水平伸縮<br />3 = 必要に応じてスペース埋め<br />4 = スペース埋め |
| 12   | bool   | $ishtml      | false | テキストがHTMLの場合にtrueとする。                           |
| 13   | bool   | $autopadding | true  | 行幅に自動調整する場合にtrueとする。                         |
| 14   | float  | $maxh        | 0     | 高さの上限                                                   |
| 15   | string | $valign      | 'T'   | 垂直方向のテキストの配置を指定する（requires $maxh = $h > 0）<br />T: TOP<br />M: middle<br />B: bottom<br /><br />$ishtmlがfalseの場合にのみ有効 |
| 16   | bool   | $fitcell     | false | trueの場合、フォントサイズを小さくしてセル内にすべてのテキストを収める。 |

### cell()メソッドとの違い
違いは、文字列の長さが長いときに自動で文字列が折り返されるかどうか。
cell()でセルを描画すると、文字列が長くセルに収まらないとき、文字列がはみ出る。
一方、multiCell()でセルを描画すると、高さは必要に応じて自動的に拡張される。つまり、文字列がセルに収まりきらない場合はセルは拡張され文字列は折り返される。

よって、可変長の文字列に下線を引く場合や、表を描画する場合はmultiCell()を使うとよい。

### 隣のセルと高さを合わせるにはgetLastH()を使う
隣のセルの高さが異なる表なんて需要がないと思うので以下は必須の対応だと思う。

getLastH()は、直近で処理したセルの高さを取得することができる。
これをmultiCell()の第二引数$hとして渡せば、セル内で文字が改行されてもセルの高さが統一される。

### 参考記事

[セル内で文字列の改行(MultiCell)](https://www.javadrive.jp/fpdf/cell/index8.html)

## PDFを出力する
**output**(\$name, \$dest)
出力時のファイル名、出力方法を文字列で指定する。

| 変数名 | 説明                                                         |
| :----- | ------------------------------------------------------------ |
| $name  | 保存時のファイル名                                           |
| $dest  | 以下のいずれかを指定。<br />I: ブラウザに出力する(既定)、保存時のファイル名が$nameで指定した名前になる。<br />D: ブラウザでダウンロードする。<br />F: ローカルファイルとして保存する。<br />S: PDFの内容を文字列として出力する。 |

# 応用
この項では、各メソッドを組み合わせて実現したことを書く。

## 二重線を書く

二重線を引くメソッドやオプションは存在しないようだったので、cell()やmultiCell()で下線付きで文字を出力したあと、line()でもう一本線を引くことで二重線のように見せた。 
2本目の線のy座標は、セル出力後の座標をgetY()メソッドで取得して設定してもよかったかもしれない。

```php
/**
 * @param string $title
 */
private function writeTitle(string $title): void
{
    // 出力開始位置の指定
    $this->tcpdf->setXY(20, 15);

    // テキストとその下線を出力
    $this->tcpdf->cell(
        65,
        15,
        $title,
        'B',
        0,
        'C',
        false,
        '',
        0,
        false,
        'C'
    );

    // さらにもう1本線を描画する
    $this->tcpdf->line(20, 23.5, 85, 23.5);
}
```

## 破線を書く

line()メソッドを使えば破線は書けるようだが、破線をこうやって描いた人もいた。
[PHPでPDF（MBFPDF） 破線と二重線](http://blog.physis.jp/article/112447005.html)

## 文字列を折り返して表示する

multiCell()メソッドを使う。
[セル内で文字列の改行(MultiCell)](https://www.javadrive.jp/fpdf/cell/index8.html)

multiCell()を使った場合は、指定した幅に文字列が収まらないときその分だけ下方向にセルが伸びる。

高さを指定すると、その高さから文字がはみ出る場合はその分はみ出して出力され、はみ出ない場合は指定した高さのセルが出力される。

ただし、文字列が折り返し表示した部分が、固定のx,y座標指定でテキスト出力している部分に被さっても、被さったまま出力されてしまう。ユーザが入力した住所やコメントなどを出力する場合は、文字列が重ならないようにひと工夫必要になる。(あるいは、可能であれば、請求書PDFで運用でカバーする・・・とか。)

対策として、multiCell()メソッドでテキストをセルとして出力したあと、その時点でのx,y座標を基準にして次の出力開始位置を決めるようにした。

```php
            $this->tcpdf->multiCell(
                85,
                6,
                '住所',
                'B',
                'L',
                false,
                1,
                10,
                $this->tcpdf->getY() + 15, // ← 相対的にy座標を指定
                true,
                0,
                false,
                false,
                );

            $this->tcpdf->multiCell(
                85,
                6,
                '氏名',
                'B',
                'L',
                false,
                1,
                10,
                $this->tcpdf->getY() + 2, // ← 相対的にy座標を指定
                true,
                0,
                false,
                false,
                );
```

### 文字列に下線を引く場合は注意が必要

multiCell()で引くことができる罫線はセルの上下左右のみであり、折り返されたテキストの途中行には下線を引くことができない。
途中行に下線を引きたければ、line()メソッドを使って「(x,y)からN行分折り返されたから、(x,y)からMmmごとにN-1行の線を引く」という処理が必要になる。

## ページ下部にページネーションを追加する
`getNumPages()` と `setPage()` を利用し、ページの下部にページ数を表示する実装をしました。

ページ数に関しては、次のようなメソッドが存在します。
`getNumPages()` ： 現在の総ページを取得する
`getPage()` : ポインタが現在何ページに存在するか取得する
`setPage()` : 指定したページにポインタを移動する
`lastPage` : 最後のページに移動する

上記メソッドを使えば、 `{現在のページ} / {ページ総数}` と出力することは容易だったのですが、
指定したページに移動したあと、multiCell()で表を描画していると、なぜかページの下部ではなく次のページに出力されてしまうという問題がありました。
そのため、y座標を初期化したところ、意図した座標(下記例では(100,285))に出力することができました。
この挙動の原因は不明ですが、text()で文字を出力するとき、恐らく直前に出力したときのy座標を基準に座標を算出しているのでは、と考えられます。そのため、y座標の初期化が有効なようです。

次のメソッドは、全ての出力を終えたあと、つまり最後のページまで出力した後に呼び出すことで1ページ目から順にページ数を出力します。

```php
    private function writePagination(): void
    {
        $totalNumOfPages = $this->tcpdf->getNumPages();
        for ($i = 1; $i <= $totalNumOfPages; $i++) {
            $this->tcpdf->setPage($i, true);
　　　　　　 // y座標を初期化
            $this->tcpdf->setY(0);
            $this->tcpdf->text(100, 285, sprintf('%s / %s', $i, $totalNumOfPages));
        }
    }
```

`{現在のページ} / {ページ総数}` と出力されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/e16a9a58-633c-59d0-e67b-005b05e2d95b.png)

## 生成したPDFを日本語ファイル名でダウンロードする
TCPDFは、output()メソッドで「D：ファイルとして保存」するか、「I：ブラウザで開く」か、などを選択できる。
[TCPDFマニュアル (勝手訳)　Output()](http://tcpdf.penlabo.net/method/o/Output.html)

しかし、この方法で実装すると、ファイル名に日本語を指定しても、日本語部分だけ出力されないという問題がある。

そのため、[EC-CUBE4標準の納品書出力機能](https://github.com/EC-CUBE/ec-cube/blob/4.0/src/Eccube/Controller/Admin/Order/OrderController.php#L700)のように、「S：PDFドキュメントの内容を文字列として出力」し、レスポンスヘッダーにいろいろと書き足すことで日本語が含まれたファイル名を指定できるようにした。

次のソースは、src/Eccube/Controller/Admin/Order/OrderController.php 710行目～

```php
 		$response = new Response(
            $orderPdfService->outputPdf(),
            200,
            ['content-type' => 'application/pdf']
        );

$downloadKind = $form->get('download_kind')->getData();

        // レスポンスヘッダーにContent-Dispositionをセットし、ファイル名を指定
        if ($downloadKind == 1) {
            $response->headers->set('Content-Disposition', 'attachment; filename="'.$orderPdfService->getPdfFileName().'"');
        } else {
            $response->headers->set('Content-Disposition', 'inline; filename="'.$orderPdfService->getPdfFileName().'"');
        }

        log_info('OrderPdf download success!', ['Order ID' => implode(',', $request->get('ids', []))]);

        $isDefault = isset($arrData['default']) ? $arrData['default'] : false;
        if ($isDefault) {
            // Save input to DB
            $arrData['admin'] = $this->getUser();
            $this->orderPdfRepository->save($arrData);
        }

        return $response;
```

### ファイル名に日本語が含まれるファイルをIEでダウンロードすると文字化ける
EC-CUBE4のソースをそのまま利用すると、ファイル名に日本語が含まれるとき、IEでダウンロードする文字化けが発生します。
(ファイルの種類に関係なく)ヘッダーの`Content-Disposition`でファイル名を指定するとき、`filename=`とは別に、`filename*=`というパラメータを併記するとIEでも名前に日本語が含まれるファイルがダウンロードできます。

```php
// IEでダウンロードすると、日本語ファイル名が文字化ける現象への対策。
// filenameを併記すると、filename*=を優先し、filename*=に対応していないブラウザはfilename=を参照する。
header("Content-Disposition: attachment; filename=\"{$fileName}\"; filename*=utf-8''" . rawurlencode($fileName));
```

#### 参考記事
 [[PHP] TCPDFで生成したPDFを日本語ファイル名でダウンロード](https://vertys.net/php-tcpdf-japanese-filename/)

### IE・EdgeでPDFをブラウザで開いてからダウンロードすると、ファイル名が強制的に書き換わる
IE・Edgeは、`Content-Disposition: inline;` としたとき(ブラウザで開くとき)、filenameを無効とする挙動をもつようです。
このとき、IEでは URLの最後のスラッシュ以降がファイル名になり、Edgeでは 無題pdf となります。
この現象について下記記事に対処方法が記載されていますが、今回はここまでやる必要は無かったので対応しませんでした。

#### 参考記事
- [[PHP] TCPDFで生成したPDFを日本語ファイル名でダウンロード](https://vertys.net/php-tcpdf-japanese-filename/)
- [サーバ側で作成したPDFをブラウザで表示させた時のファイル名を正しく設定したい](https://qiita.com/panax/items/746eb84c6ec917aebb0a)
- [サーバ側で作成したPDFの表示とクライアントPCへの保存 - 闘うITエンジニアの覚え書き](https://www.magata.net/memo/?%A5%B5%A1%BC%A5%D0%C2%A6%A4%C7%BA%EE%C0%AE%A4%B7%A4%BFPDF%A4%CE%C9%BD%BC%A8%A4%C8%A5%AF%A5%E9%A5%A4%A5%A2%A5%F3%A5%C8PC%A4%D8%A4%CE%CA%DD%C2%B8)
- [PDFをブラウザで表示](https://qiita.com/horikeso/items/31927d28289ef54653dd)

# 遭遇した問題と解決方法
## 自動改ページ機能が壊れているため自力で改ページする

setAutoPageBreak()で自動改ページすると、改ページ後にセルを出力したとき、「セルの高さが勝手に変わる」「罫線が消える」というバグが発生した。
そのため、ページ下端として定義したY座標までテキストを出力したときに改ページするように実装した。

```php
private function pageBreakIfCurrentYIsGreaterThanPageBottom(): void
{
    // A4用紙の縦は297mm。余裕をもって改ページする
    $pageBottom = 280;

    if ($this->tcpdf->getY() > $pageBottom) {
        $this->tcpdf->addPage('P', 'A4', true);
    }
}
```

## 一度に何行も出力する場合、改ページできずに見切れてしまう

先ほど280mmを基準として余裕をもって改ページしたが、この方法では例えば備考欄のように一度に何行も出力する場合、出力しきれず見切れてしまう。

### 解決策
備考欄の高さが残りのスペース(A4の縦幅297mm-余白10mm-現在Y座標)より大きければ改ページするようにした。
getNumLines()で[**簡易的に**テキストを出力するために必要な行数を取得する](http://www.t-net.ne.jp/~cyfis/tcpdf/tcpdf/getNumLines.html)ことができる。このメソッドは、改行コードが含まれる場合など実際に出力される行数と差異が出る場合があるようなので注意が必要。

```php
/**
 * @param string|null $note
 */
private function writeNote(?string $note): void
{
    $noteTitleHeight = 5;
    $numLinesOfNote = $this->tcpdf->getNumLines($note, 190, false, true, '', 1);

    // 文字列が短くても、メモ欄の高さを確保する。ここではタイトルの4行分の高さ。
    $noteHeight = max($noteTitleHeight * 4, $noteTitleHeight * $numLinesOfNote);

    if ($noteTitleHeight + $noteHeight > (287 - $this->tcpdf->getY())) {
        $this->tcpdf->addPage('P', 'A4', true);
    }

    $this->tcpdf->multiCell(
        190,
        $noteTitleHeight,
        'メモ',
        1,
        'L',
        false,
        1,
        10,
        $this->tcpdf->getY() + 8
    );

    $this->tcpdf->multiCell(
        190,
        $noteHeight,
        $note,
        1,
        'L',
        false,
        1,
        10
    );
}
```

### getBreakMargin()

この記事を書くときにgetBreakMargin()で[改ページまでの余白量を取得する](http://www.t-net.ne.jp/~cyfis/tcpdf/tcpdf/getBreakMargin.html)ことができるらしいと気付いた。
試しに使ってみたが、「現在のY座標を基準にした残りの余白スペース」は取得することができなかった。

## 半角円マークが出力できない

半角円マークをPDFに出力しようとするとバックスラッシュが出力され、[エスケープ](https://www.php.net/manual/ja/regexp.reference.escape.php)しても半角円マークを出力できなかった。

### 解決策
次の記事のようにフォントを変更したが、それも効かなかったため、全角円マーク(￥)を出力することで誤魔化した。
[PDFで文字を埋め込まない時は半角￥がバックスラッシュに変わる](https://www.okushin.co.jp/kodanuki_note/2010/07/pdf.html)

# リンク集

- [公式ドキュメント](https://tcpdf.org/)　
  - 例が豊富のようだが、ほとんど参考にしなかった。

次の二つは複数の記事で紹介されており、実装時に何度も確認した。恐らくどちらももうメンテナンスしていないだろうけど、今も残っていてよかった・・・。

- [TCPDFマニュアル (勝手訳)](http://tcpdf.penlabo.net)
  - 内容的には、tcpdf.phpのPHPDocの訳 + α(このサイトの著者による説明)だと思う。
  - TCPDF 6.2.26よりもバージョンが古いようで、時折引数名が異なっていたり、引数が重複していたりする。
- [TCPDFの部屋](http://www.t-net.ne.jp/~cyfis/tcpdf/index.html)
  - 逆引きになっている...けどその逆引きがちょっと引きにくかったりする。
  - でも「〇〇したいけどどのメソッドを使ったらいいんだ？」ってときにこのサイトを眺めるととても役に立った。