PythonでHTMLをPDFに変換する際に画像を埋め込む

## はじめに

昨今の"DX"ブームにより、自動化検討が進んでいます。レポートの自動化は誰もが思いつくDXの初手ともいうべきものです。その際に、Pythonでレポートを作成しPDFで書き換え不能にして保管する場面があるかもです。
私はさらに、そのPDFに画像を埋め込むという場面に遭遇しました。AtomエディタやVSCodeを使っている人であればMarkdownをPDFに変換するのが簡単だと思うでしょうか。今回PythonでHTMLをPDFに変換する際に、ローカルに保存した画像の扱いに手こずったため備忘録として以下の記事を残します。
### 環境

とりあえずWindows10の環境で実行します。pdfkitというライブラリが動くようであれば他のOSでもかまいません。

### 画像の準備
今回は以下のガイ・フォークスマスクをPDFに埋め込みます。権威に対する叛乱の象徴です。`'Guy-Fawkes.jpg'`という名前で保存します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19990/0f27bd33-5899-c761-1117-9c5d05687a2c.jpeg" alt="Guy-Fawkes.jpg" width="30%">

### ライブラリのインストール：pdfkit

pdfkitというPythonライブラリをインストールします。また、pdfkitのためにmkhtmltopdfというexeソフトウェアを使います。

### 画像埋め込みの要諦：Base64 URI

HTMLにローカルの画像を埋め込む際、画像をBase64 URIという文字列へ変換し、そのURIを`img`タグの`src`で参照させれば画像が表示されます。これはJupyter NotebookでノートブックからHTMLをエクスポートした際にHTMLファイルに画像が埋め込む方法と同じです（Jupyterから出力したHTMLファイルのソース中で、base64で検索するとヒットします）。

Base64 URIを参照するとPNG画像は `<img src="data:image/png;base64,..." />`のように表記されます。JEPG画像を埋め込むときは`"data:image/jpg;base64,..."`のように書きます。`"..."`の部分はハッシュ化されたような人の目で理解できない文字列になっています。非常に長くなるので基本的に割愛します。

### 実装1：素直に画像を読み出してBase64を使う

HTMLのヘッダーを追加する関数を定義します。これを使うことでHTMLの内容だけに注力して記録できます。

```python
def set_html_meta(body, lang='ja'):
    """
    <!DOCTYPE html> is essential
    """
    assert lang in ('ja', 'en'), "it can take ('ja', 'en'); but %s was fed"%lang
    html = f'<!DOCTYPE html><html lang="{lang}">\n  <meta charset="utf-8">\n'
    html += '<style> body { font-size: 1rem; } </style>\n <body>\n'
    html += body + '\n  </body>\n</html>'
    return html
```

参考：[Open base64 String Image in Jupyter Notebook Without Saving](https://stackoverflow.com/questions/55562088/open-base64-string-image-in-jupyter-notebook-without-saving)

次に、画像ファイルを`open()`で読み出して、Base64を使ってエンコードします。エンコードされた文字は`bytes`型で、通常の`string`型と異なるためPython3で比較/結合する前には`.decode()`で`string`型へ直す必要があります。

```python
import base64
imgname='Guy-Fawkes.jpg'
encoded_str = base64.b64encode(open(imgname,'rb').read())
print(type(encoded_str))
data_uri = 'data:image/jpg;base64,'+encoded_str.decode()

body="<img src='{}' width='200px'/>".format(data_uri)
html=set_html_meta(body)

with open('test.html', 'w') as out:
    """for debug purpose"""
    for row in html.split('\n'):
        out.write(row+'\n')
# <class 'bytes'>
```

最後に、出来上がったHTMLファイルをPDFへ変換します。ここでpdfkitを使います。pdfkitの設定について、`pdfkit.configuration`で指定することができます。`mkhtmltopdf.exe`へのパスを指定しないと動作しないので気を付けてください。

```python
import pdfkit
options = {
        'page-size': 'A4',
        'margin-top': '1in',
        'margin-right': '1in',
        'margin-bottom': '1in',
        'margin-left': '1in',
        'encoding': "UTF-8",
        'no-outline': None,
        'disable-smart-shrinking': '',
    }
conf_ = pdfkit.configuration(wkhtmltopdf='C:/wkhtmltox/bin/wkhtmltopdf.exe')
pdfkit.from_string(html, 'test.pdf', options=options, configuration=conf_)
#Loading pages (1/6)
#Counting pages (2/6)                                               
#Resolving links (4/6)                                                       
#Loading headers and footers (5/6)                                           
#Printing pages (6/6)
#Done                                                                      
# Out[]: True
```

出来上がった`test.pdf`を開くと、下図のように画像が埋め込まれていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19990/74610252-ed44-a8a0-4d0d-aa5502b9597f.png)

#### 躓いた点

変換したいHTMLファイルに`<!DOCTYPE html>`が記載されていないと、`pdfkit`がHTMLファイルを認識できず画像埋め込みに失敗します。正確にHTMLの文法を守るようにしましょう。

### 実装2：画像をBytesIOに書き出して`getvalue()`する

参考：[Is it possible to create encoded base64 URL from Image object?](https://stackoverflow.com/questions/16065694/is-it-possible-to-create-encoded-base64-url-from-image-object)

Base64の理解を深めるために少しExerciseします。PillowのImageオブジェクトとして読み出した画像をnumpy arrayに変換して、matplotlibで表示することができます。

```python
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from io import BytesIO
imgname='Guy-Fawkes.jpg'

# Take in base64 string and return a numpy image array
def stringToRGB(base64_string):
    imgdata = base64.b64decode(base64_string)
    image = Image.open(BytesIO(imgdata))
    return np.array(image)

byte_string=open(imgname,'rb').read()
encoded_string = base64.b64encode( byte_string )
plt.imshow(stringToRGB(encoded_string))
plt.axis('off')
plt.show()# Guy-Fawkesの画像がでる
```

上記のコードを踏まえて、BytesIOを利用していきます。以下のコードではPillowオブジェクトをBytesIOに保存し、BytesIOオブジェクトの`getvalue()`メソッドを利用したら`bytes`型が得られました。これは画像データをrawのまま取得した形で、これを`base64.b64encode`関数で変換するとBase64 URIが得られます。あとは先ほどと同じですね。

```python
imgname='Guy-Fawkes.jpg'
im = Image.open(imgname)
output = BytesIO()
im.save(output, format='JPEG')
im_data = output.getvalue()
print(type(im_data))
image_data = base64.b64encode(im_data)
if not isinstance(image_data, str):
    # Python 3, decode from bytes to string
    image_data = image_data.decode()
data_uri = 'data:image/jpg;base64,' + image_data
body="<img src='{}' width='200px'/>".format(data_uri)
html2=set_html_meta(body)
#<class 'bytes'>
```

実行例1と同様にしてPDFを保存します。

```python
options = {
        'page-size': 'A4',
        'margin-top': '1in',
        'margin-right': '1in',
        'margin-bottom': '1in',
        'margin-left': '1in',
        'encoding': "UTF-8",
        'no-outline': None,
        'disable-smart-shrinking': '',
    }
conf_ = pdfkit.configuration(wkhtmltopdf='C:/wkhtmltox/bin/wkhtmltopdf.exe')
pdfkit.from_string(html2, 'test2.pdf', options=options, configuration=conf_)
#Loading pages (1/6)
#Counting pages (2/6)                                               
#Resolving links (4/6)                                                       
#Loading headers and footers (5/6)                                           
#Printing pages (6/6)
#Done                                                                      
# Out[]: True
```

ファイルを開けば以下のように`test2.pdf`が生成されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19990/a4c1f7e6-e1a0-238c-a0e6-aaf81479131e.png)


### 応用

[Markdown](https://pypi.org/project/Markdown/)を使ってMarkdown→HTMLへの変換をしたり、[Jinja2](https://pypi.org/project/Jinja2/)を使ってMarkdownをテンプレートとして扱い動的にコンテンツを埋め込むことにすれば、機械的に様々なレポートを生成するソフトが作成できます。是非実装してみてください。

## 終わりに

V for Vendetta（映画）は何度見ても名作です。1605年の火薬陰謀事件から400年後にガイ・フォークスをモチーフにした V for Vendettaが公開されたのは非常に上手かったなと思います。

> 
“Beneath this mask there is more than just flesh. Beneath this mask there is an idea... and ideas are bulletproof."
> - Alan Moore, V for Vendetta

安直な"DX"ブームへ反抗していきましょう。

## 関連文献

- [Markdownの議事録 → HTML → PDF へ変換出力](https://qiita.com/sawa---i/items/c00ed4af45cafea70609) 
- [【Python】WebページやHTMLをPDF出力する](https://qiita.com/danishi/items/e9e0d5c1a8dae1f99b40) - wkhtmltopdfのインストールに言及している。
- [PythonでマークダウンをPDFに変換する](https://qiita.com/t_okkan/items/2efcb35c43f2bb29858e)