LibreOfficeを使って印刷物(PDF)を作る方法 準備 3/4

プログラミング

スクリプト解説

最後の記事 (“LibreOfficeを使って印刷物(PDF)を作る方法 プリンタへ出力 4/4”) にコードとテストデータ、テンプレートなどをまとめて置きますのでダウンロードして使ってください。

概要

create_pdf_test.py について解説します。

このスクリプトを前回用意した データベースファイル”住所録.odb”と”封筒テンプレート.ods” を同一のフォルダに置いてください。
前回説明した通りパスを通して以下のコマンドで動作します。

python create_pdf_test.py

完了するとフォルダ内に”pdf”フォルダが出来てその中にPDFファイルが作成されています。

サーバーモード

LibreOfficeにはサーバーモードと言う仕組みが備わっていて外部と通信できるようになっています。
今回のスクリプトもこれを使っています。
以下のコマンドでサーバーモードで起動して “–headless” オプションを付けることでバックグラウンドで動作します。

soffice --calc --accept="socket,host=localhost,port=2002;urp;" 
soffice --calc --headless --accept="socket,host=localhost,port=2002;urp;" #ヘッドレスモード

“–headless”なしでサーバーモードで起動したら以下のpythonスクリプトを実行するとCalcに接続して”A1″に”hello”と書き込まれる様子が見えます。

# soffice --calc --accept="socket,host=localhost,port=2002;urp;"

import uno

# UNOコンポーネントコンテキストの取得
local_context = uno.getComponentContext()

# UnoUrlResolverの作成
resolver = local_context.ServiceManager.createInstanceWithContext(
    "com.sun.star.bridge.UnoUrlResolver", local_context)

# 実行中のLibreOfficeに接続
ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
smgr = ctx.ServiceManager

# デスクトップオブジェクトの取得
desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)

# 現在アクティブなドキュメントを取得
doc = desktop.getCurrentComponent()

# アクティブシートの取得
sheet = doc.getCurrentController().getActiveSheet()

# セルA1に"hello"と書き込み
cell = sheet.getCellRangeByName("A1")
cell.setString("hello")

このような方法でLibreOfficeを操作しています。

スクリプト解説

説明は、コメントになっています。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# "C:\Program Files\LibreOffice\program\python.exe"

import os
import uno
import subprocess
import time
import socket
import sys
from pathlib import Path

# Unoの読み込みのためのパスを追加
sys.path.append(Path(r'C:\Program Files\LibreOffice\program'))

# LibreOfficeが接続可能になるまで待機
def wait_for_libreoffice():
    while True:
        try: # LibreOfficeに接続できるか試す
            with socket.create_connection(("localhost", 2002)):
                print("LibreOfficeに接続成功")
                return # LibreOfficeに接続できたらループを抜ける
        except OSError: # 接続できない場合は1秒待ってリトライ
            print("接続リトライ中...")
            time.sleep(1)

# LibreOffice Baseからデータを取得
def get_base_data(process, odb_name):
    # LibreOfficeが接続可能になるまで待機
    wait_for_libreoffice()

    # LibreOfficeへの接続を確立
    # LibreOfficeのUNOコンポーネントコンテキストを取得
    # つまりLibreOfficeを操作するための情報を取得するということ
    localContext = uno.getComponentContext()
    # UNO URLリゾルバを作成
    # イメージとしては、UNOが通信する際にどの名前がどの部ジェクトに対応するかを解決するためのもの
    resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext)
    # LibreOfficeへの接続を確立
    ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
    # LibreOfficeのUNOサービスマネージャを取得
    smgr = ctx.ServiceManager

    # デスクトップを取得
    # LibreOfficeでのデスクトップとは、文章を管理するためのサービス
    desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)

    # LibreOffice Baseドキュメントのロード
    odb_path = r"file:///" + Path(odb_name).resolve().as_posix()  # UNOで使うファイルパス形式
    
    # ドキュメントをロード
    doc = desktop.loadComponentFromURL(odb_path, "_blank", 0, ())

    # データソースの取得
    data_source = doc.DataSource

    # データベースへの接続
    # SAはデータベースのユーザー名、空文字はパスワード
    connection = data_source.getConnection("SA", "")

    # ステートメントを作成
    # ステートメントはSQLクエリを実行するためのもの
    statement = connection.createStatement()
    
    #
    # データベース操作開始
    #

    # SQLクエリを実行して結果を取得
    result_set = statement.executeQuery('SELECT * FROM "address"')

    # 結果を格納するリスト
    result = []

    # クエリ結果を表示
    while result_set.next():
        # カラム数を取得
        columns = result_set.getMetaData().getColumnCount()
        # 1行分のデータを取得
        row_data = []
        # 行の要素を1つずつ取得 列のインデックスは1から始まる
        for i in range(1, columns + 1):
            row_data.append(result_set.getString(i))
        #print(row_data)
        # 結果に1行分のデータを追加
        result.append(row_data)

    # リソースをクローズ
    result_set.close()
    connection.close()
    
    #
    # データベース操作終了
    #

    process.kill()  # LibreOfficeを終了
    # すべてのデータを返す
    return result


def print_data(process, calc_name, data):
    wait_for_libreoffice()

    # LibreOfficeへの接続を確立
    # データベースで説明したのと同じ
    localContext = uno.getComponentContext()
    resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext)
    ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
    smgr = ctx.ServiceManager

    # デスクトップを取得
    desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)

    # LibreOffice Calcドキュメントのロード
    template_path = r"file:///" + Path(calc_name).resolve().as_posix()  # UNOで使うファイルパス形式
    doc = desktop.loadComponentFromURL(template_path, "_blank", 0, ())

    if doc is None:
        print("ドキュメントのロードに失敗しました")
        return

    # アクティブなシートを取得
    sheet = doc.getCurrentController().getActiveSheet()

    # ドキュメント内のすべてのシェイプを取得するためにDrawPageを取得
    # DrawPageはドキュメント内の図形やテキストボックスなどのオブジェクトを管理するためのもの
    # 今回必要なのはテキストボックスなので、DrawPageからテキストボックスを取得する
    draw_pages = doc.getDrawPages()

    # シェイプの中から目的のテキストボックスを探してテキストを書き込む
    # 1行分のデータをrowに取得する
    for row in data:
        # draw_pages.getCount()でドキュメント内のDrawwPageの数を取得
        for i in range(draw_pages.getCount()):
            # draw_pages.getByIndex(i)でi番目のDrawPageを取得
            draw_page = draw_pages.getByIndex(i)
            # draw_page.getCount()でオブジェクト内のオブジェクトを数える
            for j in range(draw_page.getCount()):
                # draw_page.getByIndex(j)でj番目のオブジェクトを取得
                shape = draw_page.getByIndex(j)
                # オブジェクトがテキストボックスの場合
                if shape.ShapeType == 'com.sun.star.drawing.TextShape':
                    # テキストボックスの名前が「テキスト枠:名前」のものに書き込む
                    if shape.Name == "name":
                        shape.Text.setString(row[1] + " 様")
                    # テキストボックスの名前が「テキスト枠:住所」のものに書き込む
                    if shape.Name == "address":
                        shape.Text.setString(row[3])
                    # テキストボックスの名前が「テキスト枠:郵便番号」のものに書き込む
                    if shape.Name == "postal_code":
                        shape.Text.setString(row[2])

        # PDFとして出力
        # PDFとして出力するためにファイルパスを指定
        file_path = Path(__file__).resolve().parent / "pdf" / f"{row[0]}.pdf"
        # UNOで使うファイルパス形式に変換
        output_pdf_path = uno.systemPathToFileUrl(str(file_path))
        # PDFとして出力するためのプロパティ
        pdf_props = (
            uno.createUnoStruct("com.sun.star.beans.PropertyValue", "FilterName", 0, "calc_pdf_Export", 0),
        )
        # PDFとして出力
        doc.storeToURL(output_pdf_path, pdf_props)

    print(f"PDFとして出力されました: {output_pdf_path}")

    # LibreOfficeを終了
    process.kill()

# LibreOffice Baseからデータを取得
# LibreOfficeをサーバーモードで非同期に起動
#base_process = subprocess.Popen([r'C:\Program Files\LibreOffice\program\soffice', '--base', '--headless', '--accept=socket,host=localhost,port=2002;urp;']) #--2024.10.25 修正:サーバーモードをBaseで立ち上げるとか意味がないです。
base_process = subprocess.Popen([r'C:\Program Files\LibreOffice\program\soffice', '--headless', '--accept=socket,host=localhost,port=2002;urp;'])
data = get_base_data(base_process, '住所録.odb')
# 念のためLibreOffice Baseを終了
if base_process:
    base_process.kill()

# LibreOffice CalcをテンプレートにしてPDFを出力
#calc_process = subprocess.Popen([r'C:\Program Files\LibreOffice\program\soffice', '--calc', '--headless', '--accept=socket,host=localhost,port=2002;urp;'])  #--2024.10.25 修正:サーバーモードをCalcで立ち上げるとか意味がないです。
calc_process = subprocess.Popen([r'C:\Program Files\LibreOffice\program\soffice', '--headless', '--accept=socket,host=localhost,port=2002;urp;'])
print_data(calc_process, '封筒テンプレート.ods', data)
# 念のためLibreOffice Calcを終了
if calc_process:
    calc_process.kill()

次回は

せっかくのPDFファイルも紙に印刷したい場合もありますよね。

次回は、PDFをまとめて印刷する方法の説明をします。

やってはいないので多分の可能という話ですがCalcからプリンタに直接印刷したほうが時間はかからないのですが今回はPDFを作ってみました。

別の記事で検証のためプリンタへの直接印刷もしてみたいと思います。

LibreOfficeを使って印刷物(PDF)を作る方法 1/4

LibreOfficeを使って印刷物(PDF)を作る方法 2/4

LibreOfficeを使って印刷物(PDF)を作る方法 3/4

LibreOfficeを使って印刷物(PDF)を作る方法 4/4

Commnts