MNTSQ プラットフォーム部の藤原です。
本記事では、PythonとLibreOfficeを組み合わせたオフィスファイルのpdf変換について解説します。
LibreOfficeはオープンソースのオフィススイートです。 Microsoft Officeで作成した各種ファイル(docxや、xslx、pptx)を読み込み、編集できます。
LibreOfficeにはこれらファイルをpdfでエクスポートする機能も存在しています。 GUIからの実行はもちろんCLIでも実行可能です。
soffice --headless --convert-to pdf ファイル名.docx
LibreOfficeを導入済みの場合はこのようなコマンド1を実行することで、docxなどをpdfに変換できます。
さて、sofficeコマンドでdocxファイルなどをpdfなどで変換可能なことはここで示せました。
ウェブアプリケーションなどでオフィスファイルをpdf変換する場合には、内部でsofficeコマンドを呼び出す形で変換できそうです。
ただし、この方式では処理のたびにプロセスを立ち上げることになります。
大量のファイルを効率的に変換しようと考えると都度プロセスを立ち上げることは非効率です。
そのための対策として、LibreOfficeのプロセスを立ち上げた状態で外部プログラムからLibreOfficeの機能を利用するための仕組みが提供されています。 それが、UNO(Unified Network Objects)です。
UNO(Unified Network Objects)について
UNO(Unified Network Objects)とは、LibreOfficeの内部機能に外部プログラムからアクセスするためのコンポーネントです。 UNOは言語非依存でさまざまなプログラミング言語から利用可能です。
UNOへのアクセス方法としてはインプロセス方式とソケット接続方式があります。インプロセス方式はLibreOfficeマクロからLibreOfficeの操作をするためのものです。ソケット接続方式は外部プロセス(=外部プログラム)からTCPを使って接続してLibreOfficeの各種操作をするための仕組みです。 次のようにすることでTCP接続を介してUNOを利用できます。
soffice --headless \
--accept="socket,host=localhost,port=2002;urp;"\
--norestore
UNOを直接操作するためのPythonパッケージとしては、PyUNOが存在しますが、オフィスファイルをpdfに変換する目的としては、too muchと言えそうです。 本記事ではUNOそのものの説明については、このようにすると指定したTCPポートでLISTENさせることが可能である旨に留めておきます。 以降は、UNOを使ってLibreOfficeの機能を簡単に利用するための仕組みとしてunoserverを紹介します。
unoserverについて
unoconv/unoserverは、XML-RPC経由でUNOの仕組みを利用できるようにするための、Pythonパッケージです。
コンテナで動かす場合の動作イメージとしては以下のようになっています。

コンテナ内ではunoserverとheadlessなLibreOfficeを常時立ち上げて処理を待ち受けています2。
unoserverを簡便に利用するため、また、macOS等でも利用する際の参考としてコンテナイメージおよび、docker-compose.ymlを準備しました。 コードの全体像はFufuhu/docker-unoserverにて公開しています。
以下のようにコマンドを実行してください。
docker run -d -p 2003:2003 fufuhu/unoserver
コードからビルドして利用する場合は以下の通り実行してください。
git clone git@github.com:Fufuhu/docker-unoserver.git # dockerコマンドで利用する場合 docker build -t docker-unorsever . docker run -d -p 2003:2003 docker-unoserver # docker composeコマンドで利用する場合 docker compose up --build -d
動作確認としてPythonを使ってXML-RPCのリクエストを投げてみます。
python -c "
import xmlrpc.client;
import json;
print(json.dumps(xmlrpc.client.ServerProxy('http://localhost:2003').info()))" \
| jq
{
"unoserver": "3.6",
"api": "3",
"import_filters": {
"HTML": "HTML (StarWriter)",
〜〜中略〜〜
"impress_svg_Export": "impress_svg_Export",
"impress_tif_Export": "impress_tif_Export",
"impress_webp_Export": "impress_webp_Export",
"impress_wmf_Export": "impress_wmf_Export"
}
}
このようにして、汎用のXML-RPCクライアントを使ってunoserverを利用することが可能です。 ただし、unoserverの個別の機能を利用するには汎用のクライアントでは少々面倒です。
そこで、unoserverの提供するUnoClientを使った実装例を提示します。
UnoClientのサンプル
UnoClientはunoserverパッケージに含まれているunoserver向けのXML-RPCクライアントやバイナリデータの送受信などをラッピングした高レベルクライアントです。
Fufuhu/docker-unoserver-client-sampleにサンプルのコードを作成しました。
このリポジトリのコードをgit cloneしてdocker compose up --build -dでUNOサーバーとサンプルとなるウェブアプリケーションが立ち上がります3。
すこし本題に入るまでが長くなりますが、クライアントウェブアプリケーションのコードを眺めてみましょう。
main.pyの抜粋を見てみましょう。
from fastapi import FastAPI, Request, UploadFile from fastapi.responses import HTMLResponse, Response from fastapi.templating import Jinja2Templates from unoserver.client import UnoClient app = FastAPI() templates = Jinja2Templates(directory=Path(__file__).parent / "templates") UNOSERVER_HOST = os.getenv("UNOSERVER_HOST", "localhost") UNOSERVER_PORT = os.getenv("UNOSERVER_PORT", "2003")
FastAPIを使ったアプリケーションサーバーが立ち上がります。
UNOサーバーのホスト名と待受ポートを環境変数で指定できます。
また、テンプレートエンジンであるJinja2向けのテンプレートを格納しているディレクトリとしてtemplatesディレクトリを指定しています。
docker-compose.ymlの該当部分を抜粋すると次のようになっています。
services: unoserver: image: fufuhu/unoserver ports: - "2003:2003" app: build: . ports: - "8000:8000" depends_on: - unoserver environment: - UNOSERVER_HOST=unoserver - UNOSERVER_PORT=2003
次にmain.pyのindex関数を見てみましょう。
/にアクセスすると、index.htmlファイルをレンダリングして返すようになっています。
@app.get("/", response_class=HTMLResponse) async def index(request: Request): return templates.TemplateResponse(request, "index.html")
index.htmlのbodyタグ以下の抜粋としては以下のとおりです4。
<body> <h1>PDF変換ツール</h1> <form method="post" action="/convert" enctype="multipart/form-data"> <p>変換したいファイルを選択してください</p> <input type="file" name="file" required><br> <button type="submit">変換</button> </form> </body>
ファイルを選択して変換ボタンをクリックすると/convertにファイルがPOSTされる形となっています。
ブラウザ上でhttp://localhost:8000にアクセスすると次のような表示になります。

このフォーム内でファイルを指定して変換ボタンをクリックすると、/convertにファイルがPOSTされる形となっています。
次にmain.pyの/convertに対応するコードを見てみましょう。
@app.post("/convert") async def convert(file: UploadFile): # アップロードされたファイルの内容をバイト列として読み込む indata = await file.read() # 元のファイル名から拡張子を除いた部分を取得し、ダウンロード用のPDFファイル名を生成する stem = Path(file.filename).stem if file.filename else "output" out_filename = f"{stem}.pdf" # unoserverに接続するクライアントを作成する(XMLRPC経由で通信) client = UnoClient(server=UNOSERVER_HOST, port=UNOSERVER_PORT) # ファイルのバイト列をunoserverに送信し、PDF形式に変換する # 変換結果はPDFのバイト列として返される result = client.convert(indata=indata, convert_to="pdf") # 変換されたPDFをレスポンスとして返す # Content-Dispositionヘッダーにより、ブラウザがファイルを直接ダウンロードする return Response( content=result, media_type="application/pdf", headers={"Content-Disposition": f'attachment; filename="{out_filename}"'}, )
内容としてはコード中のコメントに記載の通りです。 リクエストからアップロードされたファイルのバイト列を取得して、UnoClientを使ってpdf変換を実現しています。
ポイントは、UnoClientです。UnoClientを利用することで、XML-RPCなどの処理を隠蔽して簡潔に記述できています。
実際の変換処理部分としては以下の2行のみで実現できます。
client = UnoClient(server=UNOSERVER_HOST, port=UNOSERVER_PORT)
result = client.convert(indata=indata, convert_to="pdf")
ここまでで、Python(unoserver, UnoClient)とLibreOfficeを使ったオフィスファイルのpdf変換について示しました。
今回提示のサンプルでは、リクエストを受けてその場で変換処理を実行して変換後のpdfファイルを返しています。 巨大なファイルを処理する場合、UXなどを考慮すると好ましい実装としてはこの限りではない点には注意した方が良いでしょう5。
まとめ
- LibreOfficeのGUI/CLIを使ってオフィスファイル(docx, xslx, pptxなど)をpdfに変換できる
- 大量のファイルを変換する場合はLibreOfficeのプロセスを常駐させ、UNO(Unified Network Objects)を使って変換する方が効率的である
- UNOを容易に利用するための仕組みとしてunoconv/unoserverが提供されている
- unoserverは言語非依存なXML-RPCを使ったファイルのpdf変換を提供している。ただし、Pythonの場合はunoserverパッケージの提供するUnoClientがあり、容易にpdf変換を実現できる
参考文献
- https://ja.wikipedia.org/wiki/XML-RPC
- https://github.com/unoconv/unoserver
- https://docs.libreoffice.org/pyuno.html
- https://hub.docker.com/r/fufuhu/unoserver
- https://github.com/unoconv/unoserver
- https://github.com/Fufuhu/docker-unoserver
- https://github.com/Fufuhu/docker-unoserver-client-sample
-
コマンド名が
sofficeとなっているのはLibreOfficeの歴史的経緯に起因するものです。基本的にはlibreofficeコマンドでもaliasが貼られていることがほとんどであり、同等の動作が可能なはずです↩ - コンテナ内部でunoserverのプロセスと、LibreOfficeのプロセス両方を管理するための仕組みとしてtiniを導入しています。↩
- Fufuhu/docker-unoserverから立ち上げたdockerコンテナやdocker composeとポート競合が発生するので、あらかじめ停止した上で実行してください。↩
- CSS指定部分などは今回はメインではないので例示からは除外しています。↩
- レスポンスで変換済みファイルを即時返却するのではなく、裏側でイベント駆動で処理したのちに変換処理完了通知とダウンロードリンクを送るなどが考えられます↩