LaTeX と Python で作る 1 ポイントたりとも表示崩れしない最強の帳票印刷ソリューション

# 元ネタ
元ネタはこちらです（以下「Figma と PHP」で略します）。

https://speakerdeck.com/ttskch/figmatophpdezuo-ru1miritaritomobiao-shi-beng-resinaizui-qiang-nozhang-piao-yin-shua-soriyusiyon

読んでなるほどと思いました。このように、誰かが苦労したおかげで後続が楽になるので感謝です。何が問題点として生じるのか、どんな解決方法が考えられるのか、が予め判明しているだけでもだいぶ楽になります。
反面、$\LaTeX$ の方が実装は簡易ではないかと思ったので、それを実践してみました。

## 条件と問題点
Figma と PHP の 44 ページに以下のような条件があります
- 改めて、満たしたい条件
- [x] ミリ単位で細かく帳票をデザインしたい。
- [x] 帳票デザインの保守性を維持するためにはビジュアルデザイン必須
- [x] 印刷時に見た目が一切崩れない

さらに、次のような障害を次々とクリアしていってます。
1. 文字参照の問題
1. 枠からのはみ出しの問題
1. 右寄せの問題
1. 自動折り返しの問題
1. 連票の問題（簡易な解説のみ）

これらを $\LaTeX$ と Python でも対処します。

## アイデアの骨子
### 既存の様式 PDF に文字（列）をスーパーインポーズ（オーバーレイ）するだけ
1. 様式 PDF と上書き用テンプレート tex ファイル (overlay.tex) を用意する。
    様式 PDF は Figma や Word などで作ってもよく、すべてを LaTeX に依存しなくても良い。
1. overlay.tex 側で様式 PDF を読み込む
1. overlay.tex には動的に値を入れたいところは ％\_顧客名\_％ とかプレースホルダーを書いておく
1. Python のスクリプトでプレースホルダーを実データで置換する
1. 置換された overlay.tex をタイプセットして PDF にする 

なおこの方法は、予めフォーマットが印刷してある帳票用紙に文字等を印字していく場合でも同じです。つまり白紙からすべて印刷するのではなく、印刷時点でスーパーインポーズする方法。その場合、デザインが終わったら様式 PDF を読み込まないようにします。

## Figma と PHP の条件に応じる
- ポイント単位[^1]で細かく帳票をデザインできる
    600DPI の印刷機を前提にすると 0.12 pt/ 1 dot なので小数第2位までの精度で解像度は充分とする（1 mm ＝ 2.83 pt）
    [^1]:ミリ単位で作成してもポイントに変換され、変換誤差も生じるので、ポイントのまま
- $\LaTeX$ は WYSIWYG ではないので条件成就は不可能。なので**あきらめる**
    が、極力ヴィジュアルデザインに寄せて位置決めできるように工夫する
- 印刷時の見た目が一切崩れないかどうかは、印刷機の性能に依存する。（結果的には成果物の PDF をプリントするだけだから）

帳票作成のための概要はこちらをご覧ください。

https://qiita.com/alien/items/a4896a966a712b8e2d04

# 実践
既存のフォーマットはこちらからとってきました。[^2] １文字ずつの位置決めがめんどくさそうだったからです。
[^2]:ただし PDF は Encrypted: yes (print:yes copy:no change:no addNotes:yes algorithm:AES) になってるので適宜解除してください。
そのままだと、タイプセット時にワーニングが出ます
extractbb:warning: PDF document is encrypted.
extractbb:warning: 001094540.pdf does not look like a PDF file...

https://www.mhlw.go.jp/content/11200000/001094540.pdf


## Python スクリプト
以下のスクリプトを使います。帳票の種類ごとにスクリプトを分けることになりそうですが、汎用的な一つのスクリプトで無理して対応するよりはメンテナンスが楽そうです。

```python:write.py
#!/usr/bin/env python
# -*- encoding: UTF-8 -*-

def main():
    with open('overlay.tex', 'r') as f: # (1)
        overlay = f.read()

    data = {
            '番号[0]': 0,
            '番号[1]': 1,
            '番号[2]': 2,
            '番号[3]': 3,
            '番号[4]': 4,
            '番号[5]': 5,
            '番号[6]': 6,
            '番号[7]': 7,
            '番号[8]': 8,
            '番号[9]': 9,
            '発生状況': '寿限無 寿限無 五却のすりきれ 海砂利水魚の 水行末 雲来末 風来末 食う寝る処に住む処 藪ら柑子の藪柑子 パイポパイポ パイポのシューリンガン シューリンガンのグーリンダイ グーリンダイのポンポコピーのポンポコナーの 長久命の 長助',
            '年月日': '1234567',
            'NUM数値1': 123,
            'NUM数値2': 123456,
            'NUM数値3': 123456789,
            '職名': '徳川次郎三郎源朝臣家康',
            } # (2)

    for key, value in data.items():
        placeholder = "%_" + key + "_%" # (3)
        if 'NUM' in key: # (4)
            value = f"{value:,}"
        overlay = overlay.replace(placeholder, str(value)) # (6)

    with open('sample.tex', 'w') as f: # (7)
         f.write(overlay)

if __name__ == "__main__":
    main()
```
- 手順は (1) テンプレートを読み込む、(2) （SQLなどの）データを dict の形に変換する、(3) プレースホルダーを決める、(6)プレースホルダーを実データで置換する、(7) タイプセッタ用ファイルに書き出す、です。
- write.py overlay.tex データ、すべて UTF8 で作れば、文字参照の問題は生じません。 (2)(3) のようにプレースホルダーに和文が使われていても問題ありません。これで、文字参照の問題は解決です。
- (4) 数値の 3 桁区切りは、Python 側にて変換します。LaTeX 側でやるより簡便です。
- 実行します。`python write.py && uplatex sample && dvipdfmx sample`

# TeX ファイル
## 位置決め補助のための PDF 作成

```tex:overlay.tex
\documentclass[uplatex,dvipdfmx,
head_space=1pt,foot_space=0pt,gutter=0pt,fore-edge=0pt, % (1)
jafontsize=11pt,
paper=a4]{jlreq} % (2)
\usepackage[deluxe,expert,uplatex,jis2004]{otf} % (3)
\usepackage{color}
\usepackage{bxpapersize} % (4)
\usepackage{jlreq-trimmarks} % (5)
\jlreqtrimmarkssetup{trimmarks_width={1pt}, color=my, banner={}} % (5)
\usepackage[abs]{overpic} %(6)

\makeatletter
\def\kintou#1#2{\leavevmode\hbox to #1{\@tfor\temp:=#2\do{\temp\hfil}\unskip}} %% 和文・欧文の均等
\usepackage{graphicx} % (7)
\newcommand*{\scaledown}[2]{%
\setbox0=\hbox{#2}%
\ifdim #1<\wd0 \resizebox{#1}{!}{#2}\else #2\fi%
}
\makeatother

\setlength{\parindent}{0pt} % (8)
\pagestyle{empty} % (9)

\begin{document}
\begin{overpic}[grid]{001094540.pdf} % (10)
\sffamily\gtfamily
% (11)
\end{overpic}
\end{document}
```

この $\TeX$ ファイルをタイプセット（`uplatex overlay && dvipdfmx overlay`）すると次のような PDF が出力されます。グリッドラインとトンボが入り、WYSIWIG じゃないハンディキャップを少しは補えるはずです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69840/e1b69201-8659-e2f2-73b6-b0ea3a7d4d58.png" width="25%">
 
- (1) 理屈上は上下左右のマージンをゼロにすべきなのですが、なぜか上のマージン位置がずれてしまったので 1pt 追加してます。[^3]（原因わかりません）
    [^3]:位置決めそのものには影響しません。overpic は、ベース画像 (PDF) の座標を使い用紙の座標ではないからです。予めフォーマットされた帳票用紙に印字する際に影響があります。
- (2)(3) 日本語を扱うとき定番の jlreq と otf です。 shebang と同じく呪文だと思っておきましょう。
- (4) 用紙サイズをよしなに設定してくれます。[LaTeX の「アレなデフォルト」 傾向と対策](https://qiita.com/zr_tex8r/items/297154ca924749e62471#%E8%BF%BD%E8%A8%98bxpapersize%E3%81%A7%E5%AE%8C%E5%85%A8%E8%A7%A3%E6%B1%BA) を参照されたし
- (5) トンボが入り、用紙が拡大します
- (6) このパッケージが肝です。PDF を読み込みます。[abs] オプションは座標を絶対位置で処理します。詳しくは [overpic – Combine LATEX commands over included graphics](https://www.ctan.org/pkg/overpic) を参照されたし
- (7) 画像を挿入したり，テキストや図の拡大縮小・回転を行うためのパッケージです。枠からのはみ出しの問題に対処するための `\scaledown{#1}{#2}` というマクロを作成します
    - #1 で上限の幅を指定し、それに収まるように文字列 #2 を表示する。というマクロです
    - 文字列 #2 の幅が #1 より小さければ、そのまま。
    - 文字列 #2 の幅が #1 より大きければ `\resizebox{#1}{!}{#2}` によりフォントサイズを縮小します
- (8) 帳票は文章じゃないのでインデントをゼロにします
- (9) 単票なのでノンブルもなし
- (10) ここで様式 PDF を読み込んで、[grid] オプションで グリッドラインを表示します
    A4 サイズが W596pt × H842pt なので細かなグリッドになってます。[^4]
    ここをただの picture 環境にすれば[^5]スーパーインポーズする文字だけの成果物になります。
    [^4]:X軸の番号が重なってしまいます。角度をつける(90°とか60°とか)オプションはないようです。
    [^5]:\\begin{picture}{596,842)〜\\end{picture}
- (11) ここにスーパーインポーズする命令を追加していきます

## スーパーインポーズの命令（プレースホルダの指定）
```tex:overlay.tex
\documentclass[uplatex,dvipdfmx,
head_space=1pt,foot_space=0pt,gutter=0pt,fore-edge=0pt, % (1)
jafontsize=11pt,
paper=a4]{jlreq} % (2)
\usepackage[deluxe,expert,uplatex,jis2004]{otf} % (3)
\usepackage{color}
\usepackage{bxpapersize} % (4)
\usepackage{jlreq-trimmarks} % (5)
\jlreqtrimmarkssetup{trimmarks_width={1pt}, color=my, banner={}} % (5)
\usepackage[abs]{overpic} %(6)
\usepackage{graphicx} % (7)

\makeatletter
\def\kintou#1#2{\leavevmode\hbox to #1{\@tfor\temp:=#2\do{\temp\hfil}\unskip}} %% 和文・欧文の均等
\makeatother

\setlength{\parindent}{0pt} % (8)
\pagestyle{empty} % (9)

\begin{document}
\begin{overpic}[grid]{001094540.pdf} % (10)
\sffamily\gtfamily
\color{red}\ebseries

%%% 労働保険番号
\put(61,722){\huge% % (12)
%_番号[0]_%%
}
\put(78,722){\huge%
%_番号[1]_%%
}
\put(95,722){\huge%
%_番号[2]_%%
}
\put(112,722){\huge%
%_番号[3]_%%
}
\put(129,722){\huge%
%_番号[4]_%%
}
\put(146,722){\huge%
%_番号[5]_%%
}
\put(163,722){\huge%
%_番号[6]_%%
}
\put(180,722){\huge%
%_番号[7]_%%
}
\put(197,722){\huge%
%_番号[8]_%%
}
\put(214.5,722){\huge% % (13)
%_番号[9]_%%
}
\put(231.5,722){\huge%
%_番号[0]_%%
}
\put(248.5,722){\huge%
%_番号[1]_%%
}
\put(265.5,722){\huge%
%_番号[2]_%%
}
\put(283,722){\huge%
%_番号[3]_%%
}

\mdseries
%%% 労働者の住所
\put(45,496){\parbox[t]{495pt}{\large\rmfamily\mcfamily% % (14)
%_発生状況_%%
}}
%%% 受付年月日
\put(423.5,759){\large\kintou{109pt}{% % (15)
%_年月日_%%
}}

%%% 受付年月日
\put(360,570){\makebox[0pt][r]{\large% % (16)
%_NUM数値1_%%
}}
\put(360,555){\makebox[0pt][r]{\large% % (16)
%_NUM数値2_%%
}}
\put(360,537){\makebox[0pt][r]{\large% % (16)
%_NUM数値3_%%
}}
\put(466,555){\scaledown{76pt}{!}{% % (17)
%_職名_%%
}}
\put(466,537){% % (17)
%_職名_%%
}
\end{overpic}
\end{document}
```
`python write.py && uplatex sample && dvipdfmx sample`を実行するとこうなります。

- 左はグリッドあり(10)、トンボあり(5)
- 中央はグリッドなし、トンボなしの成果物
- 右はフォーマットもなし（印刷時にフォーマット済みの帳票用紙に記載することを想定）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69840/4279f0a6-c193-edb7-7373-41ae35975fcd.png" width="25%">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69840/cbff5765-90d7-1544-0e61-75fa3ebb992b.png" width="25%">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69840/6abf343b-44c6-5510-d0bf-5c90f27413bc.png" width="25%" >


- (12) 座標 (61, 722) にフォントサイズ huge でテキスト %\_NUM[0]\_% を書けという意味です。
- (13) 小数点座標 (214.5, 722) も問題なし。数桁の小数でも問題ありませんが、せいぜい小数点以下 2 桁が現実的でしょう。
- (14) 座標 (45, 496) に幅 495pt の parbox を書けという意味です。parbox はその幅の中で自動的に文字列を折り返します。フォントサイズ large の明朝体でテキスト %\_発生状況\_% を書き、場合によっては折り返せ（改行）という意味です。これで、自動折り返しの問題は解決です。
- (15) １文字づつ枠内に配置せずに、均等割つけを利用して位置決めしてみました。[^6]
  [^6]:等幅フォントでないとうまくいかないはずです。和欧混植だともっとうまくいかないはずです。
- (16) 右寄せの方法です。座標(360,＊)の位置に 0pt 幅のボックス[^7]を作り、その中に右寄せでテキスト %\_NUM数値＊\_% を書けという意味です。360pt のグリッドラインにピッタリ沿っているのを確認してみてください。これで右寄せの問題は解決です。
    [^7]:makebox の前後に文字列が続かないので幅のないもので差し支えないのです。
- (17) 枠内に収まるようにフォントサイズを調整する部分です。 76pt の幅に収まるようにマクロを適用しています。職名欄で対応し、氏名欄は比較用です。 これで、枠からのはみ出しの問題は解決[^9]です。
    [^9]:当初の記事ではマクロを作らなかったのですが、重い腰を上げてマクロで対応しました。

# 連票（はペンディング）
 - （勝手に使ってよさそうな）ベースにする様式 PDF が見つからなかったからです
 - ゼロから $\LaTeX$ で作る方が簡単ですが、予めフォーマット済みの帳票用紙に配置することを想定したいからです
 - 技術的に困難だからではないです（位置決めはちょっと苦労するかも）

<!--
## 連票

https://speakerdeck.com/ttskch/figmatophpdezuo-ru1miritaritomobiao-shi-beng-resinaizui-qiang-nozhang-piao-yin-shua-soriyusiyon?slide=121

レターヘッド
ボディ（テーブル）
レターフット
連票の場合にスーパーインポーズ方式は不向きです。やってやれないことはなさそうですが、時間、労力、費用の割に合わなさそうです。なので、テーブルを作成する方法を使います。

[LaTeXでlongtableを使って長い表を貼りたい](https://qiita.com/Yu-HaruWolf/items/908dcac961f18297870c)がとても参考になります。

ここでは、xltabular パッケージを使います。longtabular とほぼ同じです。

```tex:longtable.tex
\documentclass[uplatex,dvipdfmx,
head_space=30mm,foot_space=30mm,gutter=30mm,fore-edge=30mm,
jafontsize=11pt,
paper=a4]{jlreq}
\usepackage[deluxe,expert,uplatex,jis2004]{otf}
\usepackage{bxpapersize}
\usepackage{xltabular} % (1)

\setlength{\parindent}{0pt}
\begin{document}

\begin{xltabular}{\linewidth}{|X|X|>{\raggedleft\arraybackslash}X|>{\raggedleft\arraybackslash}X|>{\raggedleft\arraybackslash}X|} % (2)

\hline \multicolumn{1}{|c|}{日付} & \multicolumn{1}{c|}{商品} & \multicolumn{1}{c|}{単価} & \multicolumn{1}{c|}{数量} & \multicolumn{1}{c|}{金額} \\ \hline 
\endhead

\multicolumn{5}{r}{次頁へ}
\endfoot

\hline
\multicolumn{5}{r}{終}
\endlastfoot
%_TABLE_%% % (3)
\end{xltabular}
\end{document}
```
- (1) テーブル作成用のパッケージをロードします
- (2) 表の形を指定します。表全体の幅を \\linewidth にして、カラムは 「左寄せ」「左寄せ」「右寄せ」「右寄せ」「右寄せ」の五つで、すべて均等幅にせよ、とういう意味です。
- (3) ここに実データを置き換えます。

```python:write2.py
#!/usr/bin/env python
# -*- encoding: UTF-8 -*-

import csv
import random
from datetime import datetime, timedelta

def main():
    with open('longtable.tex', 'r') as f:
        overlay = f.read()

    array = []
    alphabet = [chr(i) for i in range(65, 65+26)]
    for _ in range(100): # (1)
        # 日付をランダムに生成（直近1年間の日付）
        start_date = datetime.now() - timedelta(days=365)
        random_date = start_date + timedelta(days=random.randint(0, 365))
        formatted_date = random_date.strftime('%Y-%m-%d')
        # 商品をランダムに生成（仮の商品名リスト）
        product = '商品' + random.choice(alphabet)
        # 単価を1000円以上10000円未満でランダムに生成
        unit_price = random.randint(1000, 9999)
        # 数量を1以上99以下でランダムに生成
        quantity = random.randint(1, 99)
        # 行を追加
        array.append([formatted_date, product, unit_price, quantity])
    table = []
    for row in array:
        temp1 = [row[0], row[1], f"{row[2]:,}", f"{row[3]:,}", f"{row[2]*row[3]:,}"]
        table.append('&'.join(temp1)) # (2)

    placeholder = "%_TABLE_%"
    overlay = overlay.replace(placeholder, '\\\\\\hline '.join(table)) # (2)

    with open('sample.tex', 'w') as f:
         f.write(overlay)

if __name__ == "__main__":
    main()
```

- (1) ダミーデータを 100 件生成してます。テーブルデータなので 2次元配列の list 型を生成します。
- (2) LaTeX 用にテーブルデータを変換します。
    - １ 行の区切り文字は`&`です。CSV で Col,Col,Col だとすると tabular では Col&Col&Col になります。
    - 行区切りは`\\`（バックスラッシュ 2 つ）です。エスケープのため都合 4 つになってます。
    - 行区切りの水平線を書くため `\hline`を追加してます。
    - つまり
    - ```csv: csv だとこれが
      col,col,col \LF
      col,col,col \LF
      col,col,col \LF
    - ```csv: table だとこう
      col&col&col \\
      \hline
      col&col&col \\
      \hline
      col&col&col \\
      ```
あとは `python write2.py && uplatex sample && dvipdfmx sample` を実行

-->