ランダムなページサイズのPDFファイルを作成する

仕事で各ページのサイズが異なる(1ページ目がA4で、2ページ目がB5 など)PDFファイルが必要になりました。

各ページごとにサイズが異なるPDFファイルを作成できるようなツールはパッと見た感じ手近にはなかったので、Pythonで自作しました。

# 準備するもの

なお、使用した環境はWindows 10 Home 1903Verです。

* Python(3.7.5)
* PyPDF2
* ReportLab
* tqdm(使わなくてもいいです)

PyPDF2は空のPDFページを作ることができます。ただし、作成したページに**文章や画像など任意のオブジェクトを書き込むことはできません**。

ReportLabは直接プログラムコードから文字や図形などを書き込んだPDFを作ることができます。ただし、**(自分の見た限り)各ページのページサイズが異なるPDFを作ることはできません**。

なので、大まかには

1. ReportLabで1ページだけのPDFファイルを作成する
2. PyPDF2.PdfFileReaderで1を読み込む
3. PyPDF2.PdfFileWriterで作成したPDFに、2を貼り付ける

という手順をとります。ちょっと面倒くさいですね。

# コード

今回A0～C10(Cなんてあるんですね、はじめて知りましたが)まですべてのサイズのページをまんべんなく突っ込みたいので、`reportlab.lib.pagesizes`の中で定義されているページサイズをすべて取得してみました。

```py:testpdf_creator.py
import random
import os
from pathlib import Path

from tqdm import tqdm
from PyPDF2 import PdfFileReader, PdfFileWriter
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import reportlab.lib.pagesizes

YU_GOTHIC = Path(filter(lambda n: (Path(n) / "Fonts" / "YuGothB.ttc").exists(), os.environ.get("PATH").split(";")).__next__()) / "Fonts" / "YuGothB.ttc"

def makepage(writer, size, name):
  A4 = reportlab.lib.pagesizes.A4
  # テキストの書かれたPDFを作る
  c = canvas.Canvas("base.pdf", pagesize=A4)
  pdfmetrics.registerFont(TTFont("YU", YU_GOTHIC))
  c.setFont('YU', 20)
  c.drawString(12, 12, f"This is {name} page.")
  c.drawString(12, A4[1] - 50, f"This is {name} page.")
  c.rect(0, 0, A4[0], A4[1])
  c.showPage()
  c.save()

  # Create Page
  with open("base.pdf", mode="rb") as f:
    r = PdfFileReader(f)
    p = writer.addBlankPage(size[0], size[1])
    scale = [a / b for a, b in zip(size, A4)]
    p.mergeTransformedPage(r.getPage(0), [scale[0], 0, 0 , scale[1], 0, 0] , True)

if __name__ == "__main__":
  writer = PdfFileWriter()
  sizes = list(filter(lambda n: type(eval(f'reportlab.lib.pagesizes.{n}')) == tuple, dir(reportlab.lib.pagesizes)))
  s = random.sample(sizes, 10)
  for n in tqdm(s):
    makepage(writer, eval(f"reportlab.lib.pagesizes.{n}"), n)

  with open("multisize.pdf", mode="wb") as f:
    writer.write(f)
```

## 気をつけたところ

### ReportLabのCanvasについて

ReportLabでは、`Canvas`クラスを使うことでPDFファイルを作成することができます。
ただし、ここで取得できるキャンバスでは**左下が原点(X:0 Y:0)になっています**ので、図形を描画する際に注意が必要です。

それが嫌な場合は、`Canvas`クラスのコンストラクタに`bottomup=False`を追加します。

### Canvasオブジェクトのメソッドについて

`Canvas`オブジェクトのメソッド`rect()`では、次のようにX, Y座標、縦横幅のあとにいくつか引数をとります。

```py:reportlab/pdfgen/canvas.py
    def rect(self, x, y, width, height, stroke=1, fill=0):
        "draws a rectangle with lower left corner at (x,y) and width and height as given."
```
ただ、この引数の`storke`, `fill`などという値は、**線の幅や色などの値を示しているわけではなく、色を塗るかどうかのBoolean型です**。

これについては`reportlab/pdfgen/canvas.py`の44行目に以下の記述があります。

```py:reportlab/pdfgen/canvas.py
PATH_OPS = {(0, 0, FILL_EVEN_ODD) : 'n',  #no op
            (0, 0, FILL_NON_ZERO) : 'n',  #no op
            (1, 0, FILL_EVEN_ODD) : 'S',  #stroke only
            (1, 0, FILL_NON_ZERO) : 'S',  #stroke only
            (0, 1, FILL_EVEN_ODD) : 'f*',  #Fill only
            (0, 1, FILL_NON_ZERO) : 'f',  #Fill only
            (1, 1, FILL_EVEN_ODD) : 'B*',  #Stroke and Fill
            (1, 1, FILL_NON_ZERO) : 'B',  #Stroke and Fill
```

`rect`メソッドはこの`PATH_OPS`という変数を参照しており、`stroke`と`fill`という二つの引数はこの配列にある値二つに対応しているようです。

### `PyPDF2.PageObject`の`mergeScalePage`について

`PyPDF2.PdfFileWriter`でPDFに新しいページを作成するには、`PdfFileWriter#addBlankPage()`メソッドを使います。

ここに別のページオブジェクトを貼る場合は`PageObject#merge***Page()`を使います。

縮尺を変えるだけだと一見`mergeScaledPage()`あたりでいけそうですが、このメソッドの`scale`は一つの値しか受け付けないため、縦と横の縮尺が違う場合には利用できません。

縦と横の縮尺が異なる場合は、`mergeTransformedPage()`を使えば良いです。というのも、`mergeScaledPage()`は、縦横の倍率を同一値にした`mergeTransformedPage()`を呼んでいるだけなので。

もし縦を1.5倍、横を2倍の倍率に設定したい場合は、次のようにすれば良いです。

```py
page.mergeTransformedPage('追加したいPageObject', [2, 0, 0 , 1.5, 0, 0] , True)
```

### 現存するすべてのページサイズの値を取得するには

`reportlab/lib/pagesizes.py`には、すべてのページサイズの値を定義したtupleが記述されていますので、これを`dir`関数で取得しています。

ただし、このファイルにはページの縦横を逆転させる`portlait`、`landscape`などの関数も定義されているため、`filter`関数でそれらを取り除きます

```py
list(filter(lambda n: type(eval(f'reportlab.lib.pagesizes.{n}')) == tuple, dir(reportlab.lib.pagesizes)))
```

# 参考文献

* [Python PdfFileWriter Examples, PyPDF2\.PdfFileWriter Python Examples \- HotExamples](https://python.hotexamples.com/examples/PyPDF2/PdfFileWriter/-/python-pdffilewriter-class-examples.html)
* [Pythonで日本語をPDFに出力する（ReportLabを利用） \| ガンマソフト株式会社](https://gammasoft.jp/blog/pdf-japanese-font-by-python/)
* [Pythonを使用して既存のPDFにテキストを追加する \- コードログ](https://codeday.me/)
* [PyPDF2 Documentation — PyPDF2 1\.26\.0 documentation](https://pythonhosted.org/PyPDF2/)