PythonでExcel処理→PDF出力→PDF圧縮を自動化する

# はじめに
pythonでExcelの自動化は有名ですが、さらにExcelファイルをPDF出力し、そのファイルを圧縮するとこまで深掘りしようと思います。

正確には「DockerでExcel処理→PDF出力→PDF圧縮を自動化する」の方が正しいですが、ご了承ください。

最後にリポジトリも貼っているのでよろしければぜひ！

# 背景

学生団体の活動でWebアプリ開発をしました。
そのWebアプリのAPIとして「JsonからExcel操作してPDF出力」といった機能が必要になりました。
Excel操作はpythonのopenpyxlで良いとして、PDF出力するためのライブラリ(pythonに限らず)を模索しましたが、レガシーなものが多かったため最終的にlibreofficeをコンテナに導入することで機能が実装できました。

# 結論

Docker上に以下を導入することで可能になります。

|導入するもの|役割|
|-|-|
|openpyxl|Excel処理|
|libreoffice|PDF化|
|PyPDF2|PDFを扱える(今回はPDF圧縮で用いる)|

# 実装してみる
ディレクトリ構成は以下の通りとなっています。

```terminal
.
├── app
│   ├── ExcelPdfConverter.py
│   ├── files
│   │   ├── sample-compressed.pdf
│   │   ├── sample.pdf
│   │   └── sample.xlsx
│   └── sample.xlsx
├── docker
│   ├── Dockerfile
│   └── requirements.txt
└──  docker-compose.yml
```

:::note warn
補足
sample.xlsxは後ほど扱います。
:::

## requirements.txt

```text
openpyxl==3.0.10
PyPDF2==2.11.2
```

## Dockerfile

Dockerfileはできる限り[Dockerfileのベスト・プラクティス](https://docs.docker.jp/develop/develop-images/dockerfile_best-practices.html)に則っています
※libreofficeで日本語を扱うときは、日本語フォント入れないと文字化けします

```Dockerfile
FROM python:3.9-buster as builder
COPY requirements.txt /
RUN /usr/local/bin/python -m pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt

FROM python:3.9-slim-buster
ENV PYTHONUNBUFFERED=1

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        libreoffice \
        fonts-takao-gothic \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

RUN mkdir -p home/myuser/app
RUN adduser myuser && chown -R myuser home/myuser/app
USER myuser

WORKDIR /home/myuser/app
COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages/
```

## docker-compose.yml

Dockerfileでuserを作成したのでvolumesの指定を間違わないようにしてください

```yml
version: "3"

services:
  app:
    build:
      context: ./docker
      dockerfile: Dockerfile
    restart: always
    tty: true
    volumes:
      - ./app:/home/myuser/app
```

## ExcelPdfConverter.py

subprocessモジュールを利用して、pythonからlibreoffieを利用している。
※例なのであくまで参考程度にお願いします

```python
import subprocess
import os
import re
import PyPDF2
import openpyxl as px

class ExcelPdfConverter:
  def __init__(self, original_excel_file: str, out_dir:str="/home/myuser/app/files/") -> None:
    if not os.path.isfile(original_excel_file):
      raise FileNotFoundError
    if not re.search(r'.xlsx$', original_excel_file):
      raise Exception("Please input .xlsx format file")
    self.__original_excel_file: str = original_excel_file
    self.__base_name: str = os.path.splitext(os.path.basename(original_excel_file))[0]
    self.__out_dir: str = out_dir

  def editExcel(self) -> None:
    wb = px.load_workbook(self.__original_excel_file)
    ws = wb.active
    ws["A2"].value = "001"
    ws["B2"].value = "山田太郎"
    ws["C2"].value = "18"
    wb.save(f"{self.__out_dir+self.__base_name}.xlsx")

  def convertPdf(self) -> None:
    cmd: list = []
    cmd.append("libreoffice")
    cmd.append("--headless")
    cmd.append("--nologo")
    cmd.append("--convert-to")
    cmd.append("pdf")
    cmd.append("--outdir")
    cmd.append(self.__out_dir)
    cmd.append(f"{self.__out_dir+self.__base_name}.xlsx")
    subprocess.run(" ".join(cmd), shell=True)

  def pdfCompress(self) -> None:
    writer = PyPDF2.PdfFileWriter()
    reader = PyPDF2.PdfFileReader(f"{self.__out_dir+self.__base_name}.pdf")
    for i in range(reader.getNumPages()):
      page = reader.getPage(i)
      page.compressContentStreams()
      writer.addPage(page)
    with open(f"{self.__out_dir+self.__base_name}-compressed.pdf", "wb") as fp:
      writer.write(fp)

if __name__ == "__main__":
  converter = ExcelPdfConverter("sample.xlsx")
  converter.editExcel()
  converter.convertPdf()
  converter.pdfCompress()
```

### libreofficeオプション(一部抜粋)

| オプション | 概要 |
|:-:|:-:|
| headless | GUI無しで起動 |
| nologo | スプラッシュスクリーン無しで起動 |
| convert-to | 変換するファイル形式 |
| outdir | (convert-toと併用)変換するファイルの保存場所 |

:::note warn
補足
詳細は libreoffice --help で確認してください
:::

# 実際に使ってみる

ソースコードは大半xlsxファイルに依存するので流れとしては
1. 自動化させたいxlsxファイルがある
2. openpyxlでソースを書く
3. libreofficeでPDF化させる
4. PyPDF2で圧縮する

になります。

## 1.適当なxlsxを用意する(sample.xlsx)

IDと名前と歳をカラムに持つファイルを用意しました。

![スクリーンショット 2022-12-07 21.02.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1697698/a3984292-dd65-8a25-3b36-af5b44a0d957.png)

## 2. Dockerでコンテナ起動

```terminal
$ docker compose build
$ docker compose up -d
$ docker ps
CONTAINER ID ...省略
96bae4353519
$ docker exec -it 96bae4353519 /bin/bash
myuser@96bae4353519:~/app$ -ここから先はコンテナ内
```

## 3. スクリプト実行

「javaの環境ないよ」とか色々言ってきますが、気にしなくても大丈夫です。

```terminal
myuser@96bae4353519:~/app$ python ExcelPdfConverter.py
javaldx: Could not find a Java Runtime Environment!
Please ensure that a JVM and the package libreoffice-java-common
is installed.
If it is already installed then try removing ~/.config/libreoffice/4/user/config/javasettings_Linux_*.xml
Warning: failed to read path from javaldx
convert /home/myuser/app/files/sample.xlsx -> /home/myuser/app/files//sample.pdf using filter : calc_pdf_Export
Overwriting: /home/myuser/app/files//sample.pdf
myuser@96bae4353519:~/app$ 
```

### sample.pdfを見てみると

無事に出力できています

![スクリーンショット 2022-12-07 21.52.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1697698/240f8c3d-38c4-193c-e67c-903de9bff459.png)

### 圧縮の前と後のサイズを比べてみる

```terminal
$ ls -l -h app/files/
12K Dec  7 21:42 sample-compressed.pdf
13K Dec  7 21:42 sample.pdf
```

今回はシンプル過ぎたので圧縮するまでもありませんでした。
この後、別のファイルで試しましたが数百バイトの圧縮とPyPDF2の圧縮は非常に弱かったです。気持ち軽くなった程度なので、`ghostscript`とか試してみたいです。

# まとめ
1つのdockerコンテナで完結するので煩雑にならず、様々なユースケースに対応できますが、主にwebAPIとして使うことになりそうです

https://github.com/Aruminium/EditPdfPython