Hatena::ブログ(Diary)

N->N->N

2009-12-14

A. Python-UNO

Python - OpenOffice.org のブリッジである Python-UNO は OOo 本体に付属されているブリッジの一つです。短くして Py-UNO と呼ばれます。

Windows 環境であれば標準インストールでもインストールされます。また、その他の環境では openoffice.org-pyuno 関連のパッケージをインストールすることで利用できます。

OpenOffice.org 公式のビルドでは Python のランタイムが付属してきます。OOo 3.1.1 では Python 2.6.1 です。各種ディストリビューションによるビルドでは別にインストールした Python を利用していることもあります。その場合にはバージョンなどが異なることがあります。

標準の binding を利用する場合にはインターフェースのクエリなどが必要なく、オブジェクトのプロパティにも直接アクセスできます。そのため、OOo Basic と同じように楽に書くことができます。OOo Basic では配列の操作なども自分で関数を書く必要がありますが Python その他では言語が提供してくれているメソッドが利用できるため非常に楽です。

以下では標準の Python-UNO ブリッジを前提とした記述です。ほかの実装では違っているかもしれません。

ライブラリ

Py-UNO 関連のファイルは basis3.2/program/ ディレクトリ内にある以下のものです。

  • pyuno.so (または .dll) Py-UNO ブリッジの本体。直接インポートせずに uno.py を介してインポートします。
  • uno.py いくつかのクラスの定義およびオートメーションでの接続関連
  • unohelper.py コンポーネントを実装するときのヘルパー関数など
  • pythonscript.py css.script.ScriptProviderForPython の実装
  • pythonloader.py css.loader.PythonLoader の実装
  • pythonloader.uno.so

普段必要なものは uno および unohelper の二つのファイルです。

UNO の値

Python - UNO 間での値の変換はUNO Type Mappingsを参照してください。

UNO から Python 側に css.uno.XComponentContext が渡されると基本的に OOo の全てにアクセスできます。

css.uno.XComponentContext

マクロでは XSCRIPTCONTEXT 変数から取得します。

ctx = XSCRIPTCONTEXT.getScriptContext()

オートメーションでは css.bridge.XUnoUrlResolver インターフェースの resolve メソッドでコンポーネントとして StarOffice.ComponentContext を指定した返り値として取得します。詳細は下記オートメーション参照。

import uno

localctx = uno.getComponentContext()
resolver = localctx.ServiceManager.createInstanceWithContext(
    "com.sun.star.bridge.UnoUrlResolver",localctx)
ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")

コンポーネントの実装ではコンポーネントを実際に実装するクラスがインスタンス化されるときの __init__ 関数の呼び出しの際の一つ目の引数として与えられます。詳細は下記コンポーネントを参照。

サービスのインスタンス化

サービスを利用するには css.uno.XComponentContext があれば利用したいサービスをインスタンス化できます。新しいサービスのインスタンスの作成には css.lang.XMultiComponentFactory インターフェースが必要ですが XComponentContext インターフェースの getServiceManager メソッドから取得できます。

smgr = ctx.getServiceManager()
sfa = smgr.createInstanceWithContext("com.sun.star.ucb.SimpleFileAccess", ctx)
Structs

利用したい struct をインポートして新しいインスタンスを作成して利用します。

from com.sun.star.beans import PropertyValue
porp = PropertyValue()
prop.Name = "FilterName"

動的に作成したいときには次のようにします。

import uno
size = uno.createUnoStruct("com.sun.star.awt.Size")

UNO のメソッドから返された struct を変更してはいけません。変更不可能です。新しく作ったものに値を入れなおす必要があります。

Enums

Enum はインポートして利用します。

from com.sun.star.awt.WindowClass import TOP as UNO_WC_TOP

または次のようにします。

import uno
UNO_WC_TOP = uno.Enum("com.sun.star.awt.WindowClass","TOP")
Constants

定数はインポートして利用します。

from com.sun.star.beans.MethodConcept import ALL as UNO_MC_ALL

または次のようにします。

UNO_MC_ALL = uno.getConstantByName("com.sun.star.beans.MethodConcept.ALL")
例外

例外は Python の try .. except .. else .. finally で取り扱います。もちろん raise することもできます。種類を判定したり送出するには利用する例外をインポートしておく必要があります。また、UNO の例外はすべて Python の Exception クラスから派生しているため一つの except 文で UNO の例外も含めて全てトラップすることもできます。

from com.sun.star.lang import IndexOutOfBoundsException
try:
    dim = dimensions[v.get("chart:dimension")]
    indx = indexes.get(name[0:name.find("-")])
    axis = coordinate.getAxisByDimension(dim, indx)
except KeyError as e:
    pass
except IndexOutOfBoundsException as e:
    print(e)
    raise e

Python の例外と UNO の例外を別々にまとめてトラップするには css.uno.Exception も利用します。インポートするときには名前が同じなので特に指定が必要です。

from com.sun.star.uno import Exception as UNO_Exception
sequence

UNO の sequence は Python のタプルにマップされます。リストは使えません。メソッドの返り値に sequence が必要なときにはタプルを返すようにします。

from com.sun.star.beans import PropertyValue
args = []
p = PropertyValue()
p.Name = "FilterName"
p.Value = "Text - txt - csv (StarCalc)"
args.append(p)
desktop.loadComponentFromURL("file:///home/user/test.csv", "_blank", 0, tuple(args))
リスナー

必要なリスナーインターフェースを備えたクラスを作成してリスナーとします。このときリスナーは UNO コンポーネントである必要があるのでいくつかインターフェースが必要ですが次のように unohelper.Base クラスを継承すると簡単です。

import unohelper
from com.sun.star.awt import XActionListener

class ActionListener(unohelper.Base, XActionListener):
    def __init__(self):
        pass
    def actionPerformed(self, ev):
        pass
    def disposing(self, ev):
        pass

もちろん XActionListener のベースインターフェースの css.uno.XEventListener の disposing メソッドも必要です。

色々

よく必要になるいろんなこと。

URL - FilePath 変換

ファイルパスから URL。

import uno
url = uno.systemPathToFileUrl(path)

URL からファイルパス

path = uno.fileUrlToSystemPath(url)
マルチバイト文字列

UNO から渡された文字列は全て Python では unicode 型になります。Python のファイルは UTF-8 エンコードを指定しておけば問題なくマルチバイト文字を利用できます。

#!
# -*- coding: utf_8 -*-

s = u"にっぽん"
メソッドの返り値

メソッドに返り値が無い場合でも Py-UNO では None が返ります。メソッドの引数に [out] の指定がある場合にはメソッドの返り値と一緒にタプルとして渡されます。これは言語依存です。

例えば下記の css.util.XURLTransformer インターフェースの parseStrict メソッドの引数のモードは [inout] です。IDL 定義でのメソッドの実際の返り値は void ですが Python では None が返るため返り値は要素が二つのタプルになります。

from com.sun.star.util import URL
aURL = URL()
aURL.Complete = ".uno:Paste"
transformer = smgr.createInstanceWithContext(
   "com.sun.star.util.URLTransformer",ctx)
dummy, aURL = transformer.parseStrict(aURL)

[out] モードを含む返り値のタプルでは順に、メソッドの定義上の返り値、メソッドの引数のうち [out] モードのものを前から順に含みます。

Python では返り値がタプルのときには受けとるときに展開できます。

dummy, aURL = transformer.parseStrict(aURL)
# 以下と同じことです
a = transformer.parseStrict(aURL)
dummy = a[0] # 返り値のないメソッドなので None
aURL = a[1]

マクロ

利用する

まず、ツール - マクロ - マクロの管理以下に "Python..." の項目が表示されているかどうかを確認します。表示されていなければ各種環境に応じて対応するパッケージなどをインストールします。

Writer ウィンドウを開いた上で "Python..." を選択、ツリーから "OpenOffice.org Macros" - HelloWorld - HelloWorldPython を選択、実行するして "Hello World (in Python)" と表示されれば問題ありません。

Python マクロの管理では Python 用のエディタなどは用意されておらず自分でファイルを作成、気に入ったエディタで編集します。

ファイルを作成する

Python マクロのファイルは user/Scripts/python (python は小文字で)ディレクトリ以下に配置します。ファイルの拡張子が py のファイルのみが認識されます。

Python のマクロではファイル単位の実行ではなくファイルを読み込んだ後で callable (主に関数) なもの単位での実行になります。マクロの管理やマクロの実行から実行しようとするときには callable なものを調べるために一度実行されます。

python 以下にディレクトリを作成してその中にマクロのファイルを入れておくこともできます。

import

Python のマクロは pyuno 拡張モジュールの配置されたディレクトリを基点に文字列として読み込まれ、eval された後でモジュールとし、実行されます。このとき __name__ == "ooo_script_framework" です。

Python のマクロは Scripts/python におかれているにも関わらず普通に import されているわけではないため相対 import すらできません。python ディレクトリがライブラリパスに追加されているわけでもありませんしサブディレクトリにもファイルを配置できるため複雑になります。

そこで、PythonScriptProvider は実行する関数の含まれるファイルの配置されているディレクトリにある pythonpath という名前のディレクトリをライブラリパスに追加してくれます。pythonpath.zip ファイルに入れて同じディレクトリに配置することもできます。

以下のような場合に main.py ファイル中の関数を実行しようとすると pythonpath 以下の someinc.py や somemodule から import できます。

user/Scripts/python/
 |- main.py
 |- pythonpath/  <- sys.path に追加される
    |- someinc.py
    |- somemodule/
       |- __init__.py
       |- otherinc.py
実行用に表示する関数

一つのファイル中で複数の関数を定義している場合でもマクロの実行やマクロの管理ダイアログ上に表示したいものを指定したいときには次のように書きます。

g_exportedScripts = function_callable, other_callable, 

g_exportedScripts 変数にタプルで表示したい callable なものを指定します。

ScriptProvider が g_exportedScripts 変数から callable なもののタプルを調べてそれらの名前のみを表示します。

オートメーション

外部の Python を利用する

外部の Python から OOo に接続して自動化するときには OOo に付属の Python を使うのが普通です。しかし、正しく環境変数を設定すると外部の Python からでも接続できます。

Linux 系 OS で本家ビルドの開発版を /opt 以下にインストールした場合には次のようになります・・・。

UNIX 環境では次の環境変数を設定します。

  • LD_LIBRARY_PATH: libpyuno.so ファイルサーチ用
    /opt/ooo-dev3/program/../basis-link/program:/opt/ooo-dev3/program/../basis-link/ure-link/lib
  • URE_BOOTSTRAP: 起動も行うには指定が必要です
    vnd.sun.star.pathname:/opt/ooo-dev3/program/fundamentalrc

Windows 環境では PATH に追加します。

  • PATH: libpyuno.so ファイルサーチ用
    OpenOffice.org 3\URE\bin;OpenOffice.org 3\Basis\program
  • URE_BOOTSTRAP: 起動も行うには指定が必要です
    vnd.sun.star.pathname:OpenOffice.org 3/program/fundamental.ini

以下は追加の環境変数です。

  • UNO_PATH: soffice の実行ファイルがあるディレクトリパス
    /opt/ooo-dev3/program

マクロを書く環境を整える

Python のスクリプトプロバイダは IDE を提供していませんので自分で用意したエディタなどで書きます。しかし、書いては実行を繰り返すのは、慣れていないうちには実行が多くなりがちで非常に面倒です。

外部の IDE を利用してオートメーションでマクロを書くと便利です。ここでは Eclipse IDE の Pydev プラグインを利用した方法を紹介します。

  1. Eclipse に Pydev をインストールした環境を用意しておきます。
  2. Pydev プロジェクトを新しく作成して名前を付けます。このときプロジェクトのファイルを保存する場所を ~/.openoffice.org/3/user/Scripts/python 以下に指定します。
  3. プロジェクトが作られると user/Scripts/python/src ディレクトリが作られてそこにファイルが保存されるようになります。
  4. 新しいファイルを作成します。下記のようにしておくと便利です。
  5. 実行する前に外部の Python を利用するを参照して、Run Configurations... の Environment に環境変数を追加しておきます。
  6. 後はオートメーションとして実行します。

次のようなファイルを作成しておきます。uno.py、unohelper.py ファイルがインポートできるように sys.path にディレクトリを追加する必要があるかも知れません。

#!     # unopy.py
import sys
sys.path.append("/opt/ooo-dev/basis3.2/program")

import uno
import unohelper

from com.sun.star.script.provider import XScriptContext


class ScriptContext(unohelper.Base, XScriptContext):
    def __init__(self, ctx):
        self.ctx = ctx
    def getComponentContext(self):
        return self.ctx
    def getDesktop(self):
        return self.ctx.getServiceManager().createInstanceWithContext(
                "com.sun.star.frame.Desktop", self.ctx)
    def getDocument(self):
        return self.getDesktop().getCurrentComponent()

def connect():
    ctx = None
    try:
        localctx = uno.getComponentContext()
        resolver = localctx.getServiceManager().createInstanceWithContext(
            "com.sun.star.bridge.UnoUrlResolver", localctx)
        ctx = resolver.resolve(
            "uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
        if ctx:
            return ScriptContext(ctx)
    except:
        pass
    return None

そしてマクロのファイルは次のようにします。最後の if ... 以降はマクロが完成したら削除しても構いません。マクロとして実行されるときには上述の様に __name__ == "ooo_script_framework" ですので実行されません。

def HelloWorld_Writer():
    doc = XSCRIPTCONTEXT.getDocument()
    doc.getText().setString("Hello World!")


if __name__ == "__main__":
    import unopy
    
    XSCRIPTCONTEXT = unopy.connect()
    if not XSCRIPTCONTEXT:
        print("Failed to connect.")
        import sys
        sys.exit(0)

    HelloWorld_Writer()

コツはマクロと同じ実行環境にするために XSCRIPTCONTEXT 変数を模倣しておくことです。OOo が起動していない時に起動させる必要があるのであれば unopy.connect を書き換えます。

コンポーネント

unohelper.py ファイルの Base クラスは css.lang.XTypeProvider インターフェースを実装しているため新しく UNO コンポーネントを定義するときに継承すると便利です。また、登録が必要なコンポーネントを作成するときに便利な変数があります。

import unohelper

class NewUNOComponent(unohelper.Base):
    def __init__(self, ctx):
        self.ctx = ctx
    
# コンポーネントの登録
g_ImplementationHelper = unohelper.ImplementationHelper()
g_ImplementationHelper.addImplementation(
    NewUNOComponent,      # クラス (ここでは NewUNOComponent)
    IMPLEMENTATION_NAME,  # 実装名
    (SERVICE_NAME_1, SERVICE_NAME_2)) # コンポーネントを登録するサービス名のタプル

Python で UNO コンポーネントを実装したときには初期化時に ComponentContext が渡されます。css.lang.XInitialization インターフェースで初期化されるはずのコンポーネントも initialize メソッドが呼ばれないことがあるので注意が必要です (二度の初期化を防ぐためです)。

コンポーネントの登録は Python 用のコンポーネントローダがコンポーネントを実装するクラスを見つけてインスタンス化できるように指定します。実装名は実装名での検索に、サービス名はサービスのインスタンス化に利用されるため正しく指定します。コンポーネントを登録するサービスが一つのときにもタプルが渡されるようにしてください。

createInstanceWithArgumentsAndContext メソッドで初期化できるようにするには、次のようにクラスを変更します。

import unohelper
from com.sun.star.lang import XInitialization

class NewUNOComponent(unohelper.Base):
    def __init__(self, ctx, *args):
        self.ctx = ctx
        self.initialize(args)
    
    def initialize(self, args):
        pass
# 以下同じ

初期化用の引数はコンストラクタに渡され、initialize メソッドは自動的に呼ばれません。

参考

Python-UNO bridge
http://udk.openoffice.org/python/python-bridge.html
Python Scripting Framework
http://udk.openoffice.org/python/scriptingframework/index.html
トラックバック - http://d.hatena.ne.jp/hanya_orz/20091214/p1