PythonでPDFを生成するライブラリ比較まとめ

## この記事の目的

PDFで見積書を出力するにあたって関連ライブラリのメリデメを洗い出してみました。
そのライブラリの中で実装方法とアウトプットを提示し、比較できる状態にしすることが本稿の目的です。 

この記事は2つの手法のPDF出力ライブラリをリスト化して比較しました。その手法と、各種ライブラリは次のとおりです。

- HTMLをPDFを化して出力する
    - django-wkhtmltopdf
    - django_xhtml2pdf
    - WeasyPrint
- コードからPDFを生成するもの
    - reportlab

また比較に関しては次の点に注目しています。

- ライブラリの使いやすさ
    - 使うのは容易か
    - コードは複雑にならないか
    - 動作は重くないか
    - 動作させるために別途ソフトウェアのインストールは必要か
- 自由度
    - A4一枚の見積書を作成するのは容易か
    - レイアウトに対して細かい設定が可能か

- 保守性
    - メンテナンスはしやすいか
    - ライブラリの更新頻度
    - 出力されたPDFのクオリティは問題なさそうか
    - ネット上の情報は多いか

## TL;DR.

個人的な主観的な比較を置いておきます。

|   ライブラリ名 | 使いやすさ | 自由度 |  保守性  | 備考 | 
| -------- | -------- | -------- | --- | --- |
| django-wkhtmltopdf     | ○     | ○     |  △|  |
| django_xhtml2pdf     | ○     | ×     | △ | |
| WeasyPrint     | ○     | △     | △ | |
| reportlab     | ×     | ◎     |  ○| |


## HTML to PDF
### django-wkhtmltopdf

ドキュメント [django-wkhtmltopdf 3.2.0 documentation](https://django-wkhtmltopdf.readthedocs.io/en/latest/settings.html)

グーグル検索で`Python PDF`で検索したときに検索上位に出るwkhtmltopdfをdjango向けにラップされたライブラリです。
Djangoのクラスベースビューに対応しており非常に安易に導入できます。
生成されるPDFもデフォルトできれいに出力されています。ただ、別途ソフトウェアのインストールが必要なのでAppEngineでの導入の手間は大変そうです(要検証)。  


#### インストール方法

``` shell
$ pipenv install django-wkhtmltopdf
```
別途[wkhtmltopdf](https://wkhtmltopdf.org/)からソフトウェアインストールが必要。

####  SampleCode
```python=
from wkhtmltopdf.views import PDFTemplateView

class PdfSampleView(PDFTemplateView):
    filename = 'my_psdf.pdf'    
    template_name = "pdf_sample/sample.html"

```

#### 生成されたPDF

![](https://i.imgur.com/m6fNFzd.png)



## django_xhtml2pdf

PythonでHTMLをPDFに変換するライブラリ `django_xhtml2pdf`をDjango向けにラップしたライブラリです。
クラスベースビュー向けのmixinを提供されていて簡単に使用できます。またデコレーターが標準でサポートされています。  
ただしドキュメントが少なく、オプションはほとんど無い。またCSSの解釈が独特なためか通常のHTMLとは違う構成で出力されます。

[xhtml2pdf / django-xhtml2pdf](https://github.com/xhtml2pdf/django-xhtml2pdf)


### インストール方法
```shell
$ pipenv install django_xhtml2pdf
```

#### SampleCode

```python=
from django_xhtml2pdf.views import PdfMixin

class Xhtml2pdfSampleView_(PdfMixin, TemplateView):
    template_name = "pdf_sample/sample.html"
    
```
####  生成されたPDF
![](https://i.imgur.com/sp1xrXk.png)


## WeasyPrint

wkhtmltopdfに近いPDF生成ツールとライブラリ。ドキュメントが充実しています。  
wkhtmltopdfほどではないがxhtmlよりかは高品質なPDFが出力されます。xhtmlとくらべ相対的にHTMLとの出力に差分がすくない。ただ、インストールドキュメントを見るとパッケージとは別にインストールが必要とのためAppEngineではむずかしそうです(要調査)。

[インストール| WeasyPrintドキュメント](https://weasyprint.readthedocs.io/en/latest/install.html)

[WeasyPrint — WeasyPrint 51 documentation](https://weasyprint.readthedocs.io/en/latest/)

#### SampleCode
```python=
from weasyprint import HTML, CS
from django.http import HttpResponse
from django.template.loader import get_templat

class WeasyPrintView(TemplateView):
    template_name = 'pdf_sample/sample.html'

    def get(self, request, *args, **kwargs):

        html_template = get_template('pdf_sample/sample.html')
        context = super().get_context_data(**kwargs)

        html_str = html_template.render(context)  
        pdf_file = HTML(string=html_str, base_url=request.build_absolute_uri()).write_pdf(
        )

        response = HttpResponse(pdf_file, content_type='application/pdf')
        response['Content-Disposition'] = 'filename="fuga.pdf"'

        return response
```


#### 生成されたPDF
![](https://i.imgur.com/0UJaLvj.png)


## HardCodePDF

### reportlab

Pythonのコード上で実際にレイアウトを指定して生成するライブラリです。  
スタイルを含めコード上で起こすため必然的に長くなる。PDFの生成は問題なく実行できます。  
またすべてのデータをコードで挿入できるため実装の自由度は非常に高いです。  
オプションとドキュメントも充実しているため一通りの帳簿などの作成は可能です。

[ReportLab - Content to PDF Solutions](https://www.reportlab.com/)

#### インストール方法

```shell=
$ pipenv install reportlab
```

####  SampleCode


```python=
from django.views.generic import TemplateView
from django.http import HttpResponse

from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.lib.pagesizes import A4, portrait
from reportlab.platypus import Table, TableStyle
from reportlab.lib.units import mm
from reportlab.lib import colors

class ReportlabView(TemplateView):
    template_name = 'pdf_sample/sample.html'

    def get(self, request, *args, **kwargs):

        response = HttpResponse(status=200, content_type='application/pdf')
        response['Content-Disposition'] = 'filename="example.pdf"'
        # response['Content-Disposition'] = 'attachment; filename="example.pdf"'

        self._create_pdf(response)
        return response

    def _create_pdf(self, response):
        # 日本語が使えるゴシック体のフォントを設定する
        font_name = 'HeiseiKakuGo-W5'
        pdfmetrics.registerFont(UnicodeCIDFont(font_name))

        # A4縦向きのpdfを作る
        size = portrait(A4)

        # pdfを描く場所を作成：pdfの原点は左上にする(bottomup=False)
        pdf_canvas = canvas.Canvas(response)
        # ヘッダー
        font_size = 24  # フォントサイズ
        pdf_canvas.setFont("HeiseiKakuGo-W5", font_size)
        pdf_canvas.drawString(93 * mm, 770, "見積書")
        font_size = 10
        pdf_canvas.setFont("HeiseiKakuGo-W5", font_size)
        pdf_canvas.drawString(
            150 * mm, 813, f"見積発行日: "
        )
        pdf_canvas.drawString(
            150 * mm,
            800,
            "xxxxxxxxxxx-xxxxxxxxxx",
        )

        # (4) 社名
        data = [
            [f"ほげほげ会社御中", ""],
            ["案件名", "ほげほげ案件"],
            ["御見積有効限：発行日より30日", ""],
        ]

        table = Table(data, colWidths=(15 * mm, 80 * mm), rowHeights=(7 * mm))
        table.setStyle(
            TableStyle(
                [
                    ("FONT", (0, 0), (-1, -1), "HeiseiKakuGo-W5", 12),
                    ("LINEABOVE", (0, 1), (-1, -1), 1, colors.black),
                    ("VALIGN", (0, 0), (1, -1), "MIDDLE"),
                    ("VALIGN", (0, 1), (0, -1), "TOP"),
                ]
            )
        )
        table.wrapOn(pdf_canvas, 20 * mm, 248 * mm)
        table.drawOn(pdf_canvas, 20 * mm, 248 * mm)



        pdf_canvas.drawString(20 * mm, 238 * mm, "下記の通り御見積申し上げます")
        # (4) 社名
        data = [
            ["合計金額（消費税込）", f"1000 円"],
        ]

        table = Table(data, colWidths=(50 * mm, 60 * mm), rowHeights=(7 * mm))
        table.setStyle(
            TableStyle(
                [
                    ("FONT", (0, 0), (1, 2), "HeiseiKakuGo-W5", 10),
                    ("BOX", (0, 0), (2, 3), 1, colors.black),
                    ("INNERGRID", (0, 0), (1, -1), 1, colors.black),
                    ("VALIGN", (0, 0), (1, 2), "MIDDLE"),
                    ("ALIGN", (1, 0), (-1, -1), "RIGHT"),
                ]
            )
        )
        table.wrapOn(
            pdf_canvas,
            20 * mm,
            218 * mm,
        )
        table.drawOn(
            pdf_canvas,
            20 * mm,
            218 * mm,
        )

        # 品目
        data = [["内容", "開始月", "終了月", "単価", "数量", "金額"]]

        for idx in range(13):
            data.append([" ", " ", " ", " ", " ", ""])

        data.append([" ", " ", " ", "合計", "", f"{1000:,}"])
        data.append([" ", " ", " ", "消費税", "", f"{1000 * 0.10:,.0f}"])
        data.append([" ", " ", " ", "税込合計金額", "", f"{1000 * 1.10:,.0f}"])
        data.append(
            [" ", " ", " ", "", "", ""],
        )

        table = Table(
            data,
            colWidths=(70 * mm, 25 * mm, 25 * mm, 20 * mm, 20 * mm, 20 * mm),
            rowHeights=6 * mm,
        )
        table.setStyle(
            TableStyle(
                [
                    ("FONT", (0, 0), (-1, -1), "HeiseiKakuGo-W5", 8),
                    ("BOX", (0, 0), (-1, 13), 1, colors.black),
                    ("INNERGRID", (0, 0), (-1, 13), 1, colors.black),
                    ("LINEABOVE", (3, 11), (-1, 18), 1, colors.black),
                    ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
                    ("ALIGN", (1, 0), (-1, -1), "RIGHT"),
                ]
            )
        )
        table.wrapOn(pdf_canvas, 17 * mm, 100 * mm)
        table.drawOn(pdf_canvas, 17 * mm, 100 * mm)

        pdf_canvas.drawString(17 * mm, 100 * mm, "<備考>")

        table = Table(
            [[""]],
            colWidths=(180 * mm),
            rowHeights=90 * mm,
        )

        table.setStyle(
            TableStyle(
                [
                    ("FONT", (0, 0), (-1, -1), "HeiseiKakuGo-W5", 8),
                    ("BOX", (0, 0), (-1, -1), 1, colors.black),
                    ("INNERGRID", (0, 0), (-1, -1), 1, colors.black),
                    ("VALIGN", (0, 0), (-1, -1), "TOP"),
                ]
            )
        )
        table.wrapOn(pdf_canvas, 17 * mm, 5 * mm)
        table.drawOn(pdf_canvas, 17 * mm, 5 * mm)
        pdf_canvas.showPage()

        # pdfの書き出し
        pdf_canvas.save()
        


```


#### 生成されたPDF生成
![](https://imgur.com/oZymi78.png)


## 参照
[python + reportlab で 履歴書フォーマットPDFを作成 - Qiita](https://qiita.com/chiba___/items/c91ba97864efceb81f4d)  
[python 2.7 - HTML to PDF on Google AppEngine - Stack Overflow](https://stackoverflow.com/questions/49859145/html-to-pdf-on-google-appengine/49923883)