【Python】単ページのPDFを見開きで結合する方法


# 概要
[pypdf](https://pypdf2.readthedocs.io/en/latest/index.html)を使い、下図のような単ページのPDFを見開きで結合する方法を紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/ec55700a-a0b9-dc63-0cce-f6226b314dfa.png)


# 各種インポート
```shell:インストール
pip install pypdf
```
```Python:各種インポート
from pypdf import PdfWriter, PdfReader, PageObject, Transformation
```
実行時の各種バージョン
| Name | Version |
|:-:|:-:|
| pypdf | 3.2.0 |

:::note warn
注意
pypdf 3.0.0以降のバージョンで、pypdfの変数名などが大幅に変更されているので注意してください。本記事は、pypdf 3.0.0以降で機能します。
(参考:https://github.com/py-pdf/pypdf/commit/a4629d3dbdef2759ff6059446fcffd13ecf6a396)
:::

# サンプルファイル
サンプルで使用したPDFファイルです。よろしければシミュレーションとしてお使いください。

https://drive.google.com/file/d/1ziGYbEqWj6pfUvzZeJ6Bdj8EGWebQ-c8/view?usp=share_link

# 実装コード
Google Colabで作成したコードは、[こちら](https://colab.research.google.com/drive/1-xkQU_BzAH3yJpRPwYp2BjS7181sbavq?usp=share_link)にあります。

# 方法
```python:
from pypdf import PdfWriter, PdfReader, PageObject, Transformation


infile = './sample.pdf'  # 単ページのPDF
outfile = './sample_merge_2page.pdf'  # 見開きのPDFの保存ファイル

reader = PdfReader(infile)
writer = PdfWriter()

for i in range(0, len(reader.pages), 2):
    p1 = reader.pages[i]
    if i+1 == len(reader.pages):  # ページ総数が奇数の場合に、右ページに空白を補完
        p2 = PageObject.create_blank_page(width=p1.mediabox.right, height=p1.mediabox.top)
    else:
        p2 = reader.pages[i+1]
    width_1_2 = p1.mediabox.right + p2.mediabox.right
    height_1_2 = max(p1.mediabox.top, p2.mediabox.top)
    p_1_2 = PageObject.create_blank_page(width=width_1_2, height=height_1_2)

    # 見開きにするため、右ページ用のPDFを右に平行移動、mediaboxもそれに合わせて右に平行移動
    op = Transformation().translate(tx=p1.mediabox.right)
    p2.add_transformation(op)
    p2.mediabox.left = p1.mediabox.right
    p2.mediabox.right = p1.mediabox.right + p2.mediabox.right

    p_1_2.merge_page(p1)
    p_1_2.merge_page(p2)
    writer.add_page(p_1_2)

with open(outfile, mode='wb') as f:
    writer.write(f)
```
コードの説明
1. 見開きページなので左ページと右ページを2つ同時に読み込むので、``range(0, len(reader.pages), 2)``とし、``i``に左ページのindexを渡します。
1. ``p1 = reader.pages[i]``で、``p1``に左ページ用の単ページのpdfを読み込みます。
1. if文はページの総数が奇数の場合のみ、最終ページの右ページに左ページと同じサイズの余白``PageObject.create_blank_page(width=p1.mediabox.right, height=p1.mediabox.top)``を追加するため、読み込みます。
1. ``p2 = reader.pages[i+1]``で、``p2``に右ページ用の単ページのpdfを読み込みます。
1. ``width_1_2``で、見開きページの横サイズを計算します。
1. ``height_1_2``で、見開きページの縦サイズを計算します。(``max()``を使うことで、左右のページの縦サイズが異なる場合に、縦サイズの大きい方に合わせたサイズにすることができます。)
1. ``p_1_2 = PageObject.create_blank_page(width=width_1_2, height=height_1_2)``で、見開きの空白ページを作ります。この空白ページに右ページと左ページを結合していきます。
1. 見開きにしたいので、``op = Transformation().translate(tx=p1.mediabox.right)``で、右に``p1.mediabox.right``だけ平行行移動する変換変数``op``を用意します。
1. ``p2.add_transformation(op)``で、``p2``を右に``p1.mediabox.right``だけ平行移動させます。
1. ``add_transformation()``は、``mediabox``(表示領域)の情報がそのまま維持されてしまうので、``mediabox``も右に同じだけ平行移動させるために、``p2.mediabox.left``, ``p2.mediabox.right``を調整します。
1. ``p_1_2.merge_page(p1)``, ``p_1_2.merge_page(p2)``で、見開きの空白ページ``p_1_2``に、左右のページを追加します。
1. ``writer.add_page(p_1_2)``で、見開きのページを``writer``に追加します。

# 補足
<details><summary>最終ページに空白を追加せず、左ページのみ追加する方法</summary>

```python:
from pypdf import PdfWriter, PdfReader, PageObject, Transformation


infile = './sample.pdf'  # 単ページのPDF
outfile = './sample_merge_2page.pdf'  # 見開きのPDFの保存ファイル

reader = PdfReader(infile)
writer = PdfWriter()

for i in range(0, len(reader.pages), 2):
    p1 = reader.pages[i]
    if i+1 == len(reader.pages):  # ページ総数が奇数の場合に、そのまま左ページのみ追加
        writer.add_page(p1)
        break
    else:
        p2 = reader.pages[i+1]
    width_1_2 = p1.mediabox.right + p2.mediabox.right
    height_1_2 = max(p1.mediabox.top, p2.mediabox.top)
    p_1_2 = PageObject.create_blank_page(width=width_1_2, height=height_1_2)

    # 見開きにするため、右ページ用のPDFを右に平行移動、mediaboxもそれに合わせて右に平行移動
    op = Transformation().translate(tx=p1.mediabox.right)
    p2.add_transformation(op)
    p2.mediabox.left = p1.mediabox.right
    p2.mediabox.right = p1.mediabox.right + p2.mediabox.right

    p_1_2.merge_page(p1)
    p_1_2.merge_page(p2)
    writer.add_page(p_1_2)

with open(outfile, mode='wb') as f:
    writer.write(f)
```
コードの説明(変更箇所のみ)
```python:
    if i+1 == len(reader.pages):  # ページ総数が奇数の場合、そのまま左ページのみ追加
        writer.add_page(p1)
        break
```
ページが奇数の場合は左ページのみ``writer.add_page(p1)``で``writer``に追加して、それ以降の処理をスキップするため、``break``する。
</details>

# まとめ
pypdfを使い、単ページのPDFを見開きで結合する方法を紹介しました。最近、pypdf 3.0.0で変数名などが変更され、私自身PDFを見開きにする機会があり、色々とハマりどころがあったため本記事を書きました。誰かのお役に立てれば幸いです。

# 参考

https://pypdf2.readthedocs.io/en/latest/user/cropping-and-transforming.html

https://gammasoft.jp/blog/pdf-spread-by-python/