Googleスプレッドシートから年賀状の宛名を縦書きで印刷するPDFを作る

# はじめに
年賀状のシーズンですね。
年賀状の宛名作成って大変じゃないですか？
僕はここ数年、MacBookしかつかっていなくて、年賀状の宛名印刷はParallesのWindowsから筆まめを使っていました。
数年前までは他の用途でもWindowsを使うことがありましたが、今年はほとんど出番がなく、Parallelsの更新も億劫だなぁ、と感じていました。
というわけでTCPDFをつかって宛名を出力するプログラムを書いてみました。

ちなみに年賀状の投函は12月25日までだそうです。
> 2018（平成30）年用年賀はがきの引受開始は12月15日（金）からになります。
> 年賀状は12月25日（月）までにお出しください。

http://www.post.japanpost.jp/question/140.html

年末って忙しいんですよね。例年ついついオーバーしちゃいますよね。
実は僕も今月(12月)納期のプロジェクトが進んでいたのですが、年末年始は流石にリリースはしない、とのことで、納期に追われませんでした。
心安らかですね。:relaxed:

ということで年末（12月25日）に今回の「年賀状の宛名PDF作成ツール」を気張って作りました。

今回のコードは下記にあります。
https://github.com/yousan/gss-hagaki

こちらからデモも行えます。
https://posgo.l2tp.org

PosGoの使い方についての記事はこちらです
https://2017.l2tp.org/archives/809

出力はこんな感じです。

![Screen Shot 2017-12-22 at 11.04.53.png](https://qiita-image-store.s3.amazonaws.com/0/47327/fe5ca8c0-6dea-8217-fb08-a7c5815c992e.png)


# GSSのCSV取得
筆まめにはCSV形式のエクスポート機能があります。
これまで使ってきていた住所録をCSVでエクスポートして、Googleスプレッドシートで開きます。（そういえばエクセルはもうほとんど使っていないですね）

address_1、nameといった見出し行にラベルを付けておきます。
最近知ったのですが、PHPの`array_combine()`を使うとCSVからラベル行を使った連想配列が結構簡単に作れちゃうんですよね。便利！:smiley:

![Screen Shot 2017-12-22 at 11.00.31.png](https://qiita-image-store.s3.amazonaws.com/0/47327/f551b0d1-5fc8-7a0e-9096-e488a692a837.png)


GoogleスプレッドシートはURLで共有（編集も）することができます。

![Screen Shot 2017-12-22 at 11.00.54.png](https://qiita-image-store.s3.amazonaws.com/0/47327/22f0c1d7-cc0b-a633-d5ef-10759d0277f6.png)


また通常の共有状態のURLを元に、エクセル形式、CSV形式でダウンロードすることも出来ます。( @naname さんに教えてもらいましたありがとう）
ダウンロードの手順は以下の通りです。

1. 共有用URLの後ろの部分、最後のパスを /editから /exportにする
1. クエリストリングで `format=csv` を付ける

こうすることでダウンロードすることができます。

手順をPHPにすると下記の通りです。

```php
    /**
     * GoogleSpreadSheetのURLが正しいかチェックし、CSVダウンロード用に修正する。
     *
     * インプット例 https://docs.google.com/spreadsheets/d/1yfMIdt8wgBPrMY3UwiCTsX3EN_2gcLCmPAEy8dfYeLY/edit?usp=sharing
     * @param $url
     *
     * @return string
     * @throws Exception
     */
    private function fixURL($url) {
        // 1. docs.google.comで始まっている
        // 1. URLパスの最後がexportになっている

        // またフォーマットについては正しくない場合に修正を行う。
        // 1. format=csvになっている

        // 先頭が https://docs.google.com/ で始まっているか確認する ココ重要！
        if ( 0 !== strpos($url, 'https://docs.google.com/spreadsheets')) {
            throw new Exception('GoogleスプレッドシートのURLではないようです。');
        }

        // 末尾の/editを/exportに変える（厳密にはURL中の…、だけれど、ハッシュで/editが出る可能性は低いと見ている
        // e.g. https://docs.google.com/spreadsheets/d/1yfMIdt8wgBPrMY3UwiCTsX3EN_2gcLCmPAEy8dfYeLY/edit#gid=0
        $url = str_replace('/edit', '/export', $url);

        // #gid=0があれば取り除く
        // e.g. https://docs.google.com/spreadsheets/d/1yfMIdt8wgBPrMY3UwiCTsX3EN_2gcLCmPAEy8dfYeLY/export#gid=0
        $url = str_replace('#gid=0', '', $url);

        // 末尾に?format=csvを足す
        // e.g. https://docs.google.com/spreadsheets/d/1yfMIdt8wgBPrMY3UwiCTsX3EN_2gcLCmPAEy8dfYeLY/export
        if ( FALSE === strpos($url, 'format=csv') ) {
            // @link https://stackoverflow.com/questions/5809774/manipulate-a-url-string-by-adding-get-parameters
            $query = parse_url($url, PHP_URL_QUERY); // クエリ文字列だけを抜きだす
            $url .= !empty($query) // 既にクエリ文字列が設定されているかどうか
                ? '&format=csv' // 設定されていれば&で連結し
                : '?format=csv'; // そうでなければ?で連結する
        }

        // 完成したURLの例
        // e.g. https://docs.google.com/spreadsheets/d/1yfMIdt8wgBPrMY3UwiCTsX3EN_2gcLCmPAEy8dfYeLY/export?usp=sharing&format=csv
        return $url;
    }
```

これでスプレッドシートからデータを拾うことができます。


# はがきの枠の作成とTCPDFの準備
PHPとTCPDFを使ってはがき用のPDFを出力します。

TCPDFでは縦横の用紙のサイズ、利用する単位（ミリメートル）、用紙の向き（ポートレートとランドスケープ）などを設定します。
また日本語用のフォントも追加しておきます。

http://mix-mplus-ipa.osdn.jp/migmix/

```php
    public function defineHagaki()
    {
        $this->pdf = new TcpdfFpdi('P', 'mm', [100, 148]);
        // PDFの余白(上左右)を設定
        $this->pdf->SetMargins(0, 0, 0, true);
        // ヘッダーの出力を無効化
        $this->pdf->setPrintHeader(false);
        // フッターの出力を無効化
        $this->pdf->setPrintFooter(false);

        // 手動で追加する場合
        $this->font       = new TCPDF_FONTS();
        $this->fontfamily = $this->font->addTTFFont(self::FONT);

        $this->pdf->SetFont($this->fontfamily, '', 11);

        // 書き込む文字列の文字色を指定
        $this->pdf->SetTextColor(94, 61, 28);
        // デフォルト行間
        $default_cell_height_ratio = $this->pdf->getCellHeightRatio();

        // 自動改ページ @link http://www.t-net.ne.jp/~cyfis/tcpdf/tcpdf/SetAutoPageBreak.html
        $this->pdf->SetAutoPageBreak(false, 0);

        mb_internal_encoding('UTF-8');
    }
```

位置調整を行うために、去年の年賀はがきをスキャンしてトレースしました。
はがきの枠は`TCPDF::setSourceFile()`というメソッドで定義できます。
年賀状は複数枚（多ければ１００枚とか）を印刷する前提ですので、PDFの中に宛名の数だけページを設けます。
改ページで独立したメソッドとしておきます。

```php
    /**
     * 改ページ。
     */
    public function addPage() {
        // ページを追加
        $this->pdf->AddPage();
        if ( (boolean)$this->use_template ) {
            // テンプレートを読み込み
            $this->pdf->setSourceFile(self::BASEPDF);
            $tplIdx = $this->pdf->importPage(1);
            // 読み込んだPDFの1ページ目をテンプレートとして使用
            $this->pdf->useTemplate($tplIdx, null, null, null, null, true);
        }
    }
```

# 郵便番号の位置を調整する
年賀状に限らず、郵便はがきでは郵便番号を出力する必要があります。
郵便番号は機械で自動的に仕分けられるそうです。
手書きだったりしてもある程度判別してくれるらしいのですが、認識率を高めるためには枠内にキレイに書き出す必要があります。

```php
    /**
     * 郵便番号を設定する。
     *
     * @param string $zipcode
     */
    public function zipcode($zipcode)
    {
        $this->pdf->SetFont($this->fontfamily, '', 19);
        $this->pdf->Text(45, 10, $zipcode[0]);
        $this->pdf->Text(52, 10, $zipcode[1]);
        $this->pdf->Text(59, 10, $zipcode[2]);
        $this->pdf->Text(67, 10, $zipcode[3]);
        $this->pdf->Text(74, 10, $zipcode[4]);
        $this->pdf->Text(81, 10, $zipcode[5]);
        $this->pdf->Text(88, 10, $zipcode[6]);
    }

    /**
     * 差出人の郵便番号を設定する。
     *
     * @param $zipcode
     */
    public function owner_zipcode($zipcode)
    {
        $this->pdf->SetFont($this->fontfamily, '', 12);
        $this->pdf->Text(3.75, 124.5, $zipcode[0]);
        $this->pdf->Text(7.75, 124.5, $zipcode[1]);
        $this->pdf->Text(11.75, 124.5, $zipcode[2]);
        $this->pdf->Text(17, 124.5, $zipcode[3]);
        $this->pdf->Text(21.25, 124.5, $zipcode[4]);
        $this->pdf->Text(25.5, 124.5, $zipcode[5]);
        $this->pdf->Text(29.75, 124.5, $zipcode[6]);
    }
```

郵便番号については高さ（Yの値）は変わらないので、一定で良いですね。

こんな感じで出力されます。

![Screen Shot 2017-12-22 at 11.03.54.png](https://qiita-image-store.s3.amazonaws.com/0/47327/f13fcb22-d16a-9729-2de4-7eaa843f300b.png)



# PDFで縦書きを実現する

縦書きってすごく大変ですよね…。文字コード問題とか縦書きとかの実装するときには、「なんて日本語って面倒なんだろう」って思っちゃいますよね。
でも年賀状はどうしても縦書きじゃないとダメですよね。

ということで縦書きにしました。

次の記事が大変参考になりました。

https://dbweb.0258.net/wiki.cgi?page=tcpdf%A4%C7%C6%FC%CB%DC%B8%EC%A4%CE%BD%C4%BD%F1%A4%AD

具体的には

1. １文字ずつの高さを計算する
1. １文字出力して、高さをずらす
1. 以下ループ...

という方法です。

それぞれ高さを算出した後に `TCPDF::Text()`で出力して、位置を進めています。

```php
for ($i = 0; $i < mb_strlen($str); $i++) { // 各文字でループ
  $this->pdf->Text($x, $y, $c);
  $y += $fh; // 高さを一文字分だけ進める
}
```

また当初はほぼ参考サイト通りにやっていたのですが、２点工夫しました。
宛名特有の問題になる下記の２点です。

1. 番地、部屋番号の（英）数字は横につなげたい
1. 差出人欄は下段揃えにしたい

どちらも宛名特有の問題ですね。

ということで、連続する半角文字をストックして出力するように修正します。

```php
$hankaku_str = '';
for ($i = 0; $i < mb_strlen($str); $i++) { // 各文字でループ
    $c = mb_substr($str, $i, 1, 'UTF-8'); // 一文字だけ取り出す
    if ( $this->isHankaku($c) ) { // 半角文字列が来た場合ストックする
        $hankaku_str .= $c;
    } else { // 全角文字だった場合
        if ( !empty($hankaku_str) ) { // 全角文字が出るまでに半角文字がストックされていた場合、放出する
            $this->hankakuYoko($x, $y, $size, $hankaku_str);
            $hankaku_str = ''; // ストックをゼロに
            $y += $fh; // 高さを一文字分だけ進める
        }
        $this->pdf->Text($x, $y, $c);
        $y += $fh; // 高さを一文字分だけ進める
    }
}
```

ということでそれらを踏まえたコードが下記です。

```php

/**
 * 文字を縦書きに配置する関数
 * thanks! @link https://dbweb.0258.net/wiki.cgi?page=tcpdf%A4%C7%C6%FC%CB%DC%B8%EC%A4%CE%BD%C4%BD%F1%A4%AD
 *
 * @param $x
 * @param $base_y
 * @param $str
 * @param int $size
 * @param bool $sitatsuki 下付き文字（下段揃え）の文字列の場合。
 * @param float $height_ratio ここで指定されたサイズを基に縦書きの字間を計算する
 *
 * @internal param $y
 */
private function tate1($x, $base_y, $str, $size, $sitatsuki = false, $height_ratio = 1.0)
{
    $this->pdf->SetFont($this->fontfamily, '', $size);
    $fh = $this->pt2mm($size * $height_ratio); // 文字のサイズから算出される1文字の大きさ(高さ)
    $str = $this->hyphenation($str); // ハイフンを縦棒に

    $l = $this->mb_tate_strlen($str);
     if ($sitatsuki) { // 下付きの場合
        // 下付き（下段揃え）の場合には開始位置を事前に計算しておく。
         $l = $this->mb_tate_strlen($str);
        $y = $base_y - ( $fh * $l );
    } else {
        $y = $base_y;
    }

    $hankaku_str = '';
    for ($i = 0; $i < mb_strlen($str); $i++) { // 各文字でループ
        $c = mb_substr($str, $i, 1, 'UTF-8'); // 一文字だけ取り出す
        if ( $this->isHankaku($c) ) { // 半角文字列が来た場合ストックする
            $hankaku_str .= $c;
        } else { // 全角文字だった場合
            if ( !empty($hankaku_str) ) { // 全角文字が出るまでに半角文字がストックされていた場合、放出する
                $this->hankakuYoko($x, $y, $size, $hankaku_str);
                $hankaku_str = ''; // ストックをゼロに
                $y += $fh; // 高さを一文字分だけ進める
            }
            $this->pdf->Text($x, $y, $c);
            $y += $fh; // 高さを一文字分だけ進める
        }
    }
    // ループが終わりきって半角がストックされていた場合、最後の出力を行う。
    if ( !empty($hankaku_str) ) {
        $this->hankakuYoko($x, $y, $size, $hankaku_str);
        $hankaku_str = ''; // ストックをゼロに
        $y += $fh; // 高さを一文字分だけ進める
    }
}

/**
 * 縦書きにした時の文字列長を計算する。
 * ポイントとしては、連続する半角文字については横書きになるので、１文字として計算する。
 * e.g. 'あいうABCえおCDほげ' => 9
 * e.g. 'あいうえおほげ' => 7
 *
 * @param $str
 *
 * @return int
 */
private function mb_tate_strlen($str) {
    // e.g. 'あいうABCえおCDほげ' の場合、['ABC', 'CD']がそれぞれ1文字になるので、
    // 全文字列長( mb_strlen('あいうABCえおCDほげ') )から
    // -2 ( ABC.length() - 1), -1 (CD.length() -1 )) = -3 文字をオフセットで引きたい
    $length = mb_strlen($str);
    if (preg_match_all('/(?<hankaku>[A-z0-9\-]+)/', $str, $matches)){
        foreach ($matches['hankaku'] as $key => $value ){
            // 1文字だけの半角文字列だった場合には引かない。
            if (strlen($value) > 1 ) { // 2文字以上の連続する長さnの半角文字列だった場合、n-1分だけ引く。
                $length -= strlen($value) - 1;
            }
        }
    }
    return $length;
}

/**
 * 半角、全角を判定する
 * @link https://singoro.net/note/count-utf8/
 *
 * @param string $c 文字
 *
 * @return bool
 */
private function isHankaku( $c ) {
    if ( ( mb_strwidth(trim($c), 'UTF-8') / 2 ) === 0.5 ) {
        return true;
    } else {
        return false;
    }
}

```

こんな感じでできました。

![Screen Shot 2017-12-22 at 11.06.02.png](https://qiita-image-store.s3.amazonaws.com/0/47327/9aa4441f-efc7-7da9-a07c-1f92e9320834.png)

うーん…、半角英数字がちょっと右にズレているような気がしなくもないですが…。まぁ合格点ですね！

# 出来上がったPDF
というわけで出来上がったPDFはこちらです。

![Screen Shot 2017-12-22 at 11.03.40.png](https://qiita-image-store.s3.amazonaws.com/0/47327/2788a79c-5fab-3e30-5545-1b78633374a6.png)


印刷してないですが…、ちゃんと出来てる気がしますね！

こちらのプログラムを組み込んだデモサイトも作りました。

![Screen Shot 2017-12-22 at 11.06.52.png](https://qiita-image-store.s3.amazonaws.com/0/47327/e87c1055-b994-dab0-d4b9-4e6414943da1.png)


https://posgo.l2tp.org

Googleスプレッドシートの共有URLを入れるとPDF化してくれます。
こちらのサイトのフロント周りはすべてシュンゴ＝イワサキ氏に作ってもらいました。ありがとうございます！

こちらのサイトの運用には注意を払っていますが、利用に際してはセキュリティリスクに注意してください。

もちろん下記のコードでもPDFは作成可能ですので、是非手元で動かしてください。

https://github.com/yousan/gss-hagaki


# ライセンスと謝辞
フォントはMigMixを利用しました。ありがとうございます。
このフォントのライセンスはIPAです。
http://mix-mplus-ipa.osdn.jp/migmix/

# 参考サイト

https://qiita.com/emegane/items/486975a79ebb267c2b8e
https://qiita.com/noratmt/items/d0bc4bc95eaf92d07ca6