LibreOffice5(105)埋め込みマクロフォルダを取り出すスクリプト

2017-12-09

旧ブログ

t f B! P L
ドキュメントファイルはzip形式でまとめられているだけなので、zipコマンドで簡単に取り出せます。(GUIでやる場合はLibreOffice(39)7-ZipのGUIで楽にPythonマクロをドキュメントに埋め込む参照)。今回はそれ以外の方法も考えます。

前の関連記事:LibreOffice5(104)Packageのサービスとインターフェイスの一覧


埋め込みマクロフォルダを取り出す方法を考える


まず読み込むドキュメントの状態として、LibreOfficeで開いている場合(ディスク上に保存されていない場合も含む)と開いていない場合(ディスク上に保存されている場合)に分けられます。

LibreOfficeでドキュメントを開いている場合は、ドキュメント内のファイルについてvnd.sun.star.tdocから始まるfileurlを使えます。

それによってSimpleFileAccessのcopy()メソッドで簡単にマクロファイルを取り出せます。

LibreOfficeでドキュメントを開いていない場合は、ドキュメント内のファイルについて、vnd.sun.star.pkgで始めるfileurlを使えばLibreOffice5(109)子要素にフォルダしか持たないドキュメント内フォルダの場合を除いて同様に取り出せました(LibreOffice5(108)ドキュメントストレージからファイルシステムストレージにデータをコピーする の「LibreOfficeで開いていないドキュメント内へのURIを使う」参照)。

他にディスク上に保存されているドキュメントの中にアクセスする方法として、
ドキュメント(モデル)のXStorageBasedDocumentインターフェイスのgetDocumentStorage()メソッドでストレージを取得する方法、
PackageサービスでPackageFolderを取得する方法、
ZipFileAccessサービスで取得する方法、
の3つがあります。

取り出したファイルを書き出すには、ファイル(ストリーム)は
SimpleFileAccessのwriteFile()メソッドでファイルに書き出し、
フォルダはSimpleFileAccessのcreateFolder()メソッドで作成します。

いちいち各ファイルについてSimpleFileAccessのwriteFile()メソッドで書き出さずに、FileSystemStorageFactoryサービスで書き出し先のフォルダのストレージを取得して、そのストレージにcopyToStorage()でドキュメント内のストレージをコピーしようとしましたがそれはうまくいきませんでした。

その代わりに各ストリームをインプットストリームとアウトプットストリームで書き出すことはできました(LibreOffice5(108)ドキュメントストレージからファイルシステムストレージにデータをコピーする参照。)。

LibreOfficeで開いているドキュメントから埋め込みマクロフォルダを取り出すスクリプト


SimpleFileAccessのcopy()メソッドを使う方法です。

一つ上の階層にあるodsファイルを一つ取得して、そのファイル内のマクロフォルダを取得したodsファイルと同じ階層のsrcフォルダ内に書き出します。

埋め込みマクロがあるodsファイルを用意して、そのファイルを開いておかないといけません。

そのファイルのfileurl(vnd.sun.star.tdocプロトコール)を元にその開いているドキュメントモデルを取得しているので、LibreOfficeのバンドルPythonをインタープリターにして実行しないといけません。
import unohelper  # オートメーションには必須(必須なのはuno)。
import glob
import os
def macro():  # マクロでは利用不可。docを取得している行以降のみはマクロで実行可。 
 ctx = XSCRIPTCONTEXT.getComponentContext()  # コンポーネントコンテクストの取得。
 smgr = ctx.getServiceManager()  # サービスマネージャーの取得。 
 simplefileaccess = smgr.createInstanceWithContext("com.sun.star.ucb.SimpleFileAccess", ctx)  # SimpleFileAccess
 os.chdir("..")  # 一つ上のディレクトリに移動。
 ods = glob.glob("*.ods")[0]  # odsファイルを取得。最初の一つのみ取得。
 systempath = os.path.join(os.getcwd(), ods)  # odsファイルのフルパス。
 doc_fileurl = unohelper.systemPathToFileUrl(systempath)  # fileurlに変換。
 desktop = ctx.getByName('/singletons/com.sun.star.frame.theDesktop')  # デスクトップの取得。
 components = desktop.getComponents()  # ロードしているコンポーネントコレクションを取得。
 for component in components:  # 各コンポーネントについて。
  if hasattr(component, "getURL"):  # スタートモジュールではgetURL()はないため。
   if component.getURL()==doc_fileurl:  # fileurlが一致するとき、ドキュメントが開いているということ。
    doc = XSCRIPTCONTEXT.getDocument()  # 開いているドキュメント(モデル)を取得。
    transientdocumentsdocumentcontentfactory = smgr.createInstanceWithContext("com.sun.star.frame.TransientDocumentsDocumentContentFactory", ctx)
    transientdocumentsdocumentcontent = transientdocumentsdocumentcontentfactory.createDocumentContent(doc)
    contentidentifierstring = transientdocumentsdocumentcontent.getIdentifier().getContentIdentifier()  # ex. vnd.sun.star.tdoc:/1
    python_fileurl = "/".join((contentidentifierstring, "Scripts/python"))  # ex. vnd.sun.star.tdoc:/1/Scripts/python
    dest_dir = createDest(simplefileaccess)  # 出力先フォルダのfileurlを取得。
    if simplefileaccess.exists(python_fileurl):  # 埋め込みマクロフォルダが存在するとき。
     simplefileaccess.copy(python_fileurl, dest_dir)  # 埋め込みマクロフォルダを出力先フォルダにコピーする。
    else:
     print("The embedded macro folder does not exist in {}.".format(ods))
    break 
 else:
  print("{} does not loaded.".format(ods))
def createDest(simplefileaccess):  # 出力先フォルダのfileurlを取得する。
 src_path = os.path.join(os.getcwd(), "src")  # srcフォルダのパスを取得。
 src_fileurl = unohelper.systemPathToFileUrl(src_path)  # fileurlに変換。
 destdir = "/".join((src_fileurl, "Scripts/python"))
 if simplefileaccess.exists(destdir):  # pythonフォルダがすでにあるとき
  simplefileaccess.kill(destdir)  # すでにあるpythonフォルダを削除。 
 simplefileaccess.createFolder(destdir)  # pythonフォルダを作成。
 return destdir 
if __name__ == "__main__":  # オートメーションで実行するとき
 def automation():  # オートメーションのためにglobalに出すのはこの関数のみにする。
  import officehelper
  from functools import wraps
  import sys
  from com.sun.star.beans import PropertyValue  # Struct
  from com.sun.star.script.provider import XScriptContext  
  def connectOffice(func):  # funcの前後でOffice接続の処理
   @wraps(func)
   def wrapper():  # LibreOfficeをバックグラウンドで起動してコンポーネントテクストとサービスマネジャーを取得する。
    try:
     ctx = officehelper.bootstrap()  # コンポーネントコンテクストの取得。
    except:
     print("Could not establish a connection with a running office.", file=sys.stderr)
     sys.exit()
    print("Connected to a running office ...")
    smgr = ctx.getServiceManager()  # サービスマネジャーの取得。
    print("Using {} {}".format(*_getLOVersion(ctx, smgr)))  # LibreOfficeのバージョンを出力。
    return func(ctx, smgr)  # 引数の関数の実行。
   def _getLOVersion(ctx, smgr):  # LibreOfficeの名前とバージョンを返す。
    cp = smgr.createInstanceWithContext('com.sun.star.configuration.ConfigurationProvider', ctx)
    node = PropertyValue(Name = 'nodepath', Value = 'org.openoffice.Setup/Product' )  # share/registry/main.xcd内のノードパス。
    ca = cp.createInstanceWithArguments('com.sun.star.configuration.ConfigurationAccess', (node,))
    return ca.getPropertyValues(('ooName', 'ooSetupVersion'))  # LibreOfficeの名前とバージョンをタプルで返す。
   return wrapper
  @connectOffice  # createXSCRIPTCONTEXTの引数にctxとsmgrを渡すデコレータ。
  def createXSCRIPTCONTEXT(ctx, smgr):  # XSCRIPTCONTEXTを生成。
   class ScriptContext(unohelper.Base, XScriptContext):
    def __init__(self, ctx):
     self.ctx = ctx
    def getComponentContext(self):
     return self.ctx
    def getDesktop(self):
     return ctx.getByName('/singletons/com.sun.star.frame.theDesktop')  # com.sun.star.frame.Desktopはdeprecatedになっている。
    def getDocument(self):
     return self.getDesktop().getCurrentComponent()
   return ScriptContext(ctx)  
  return createXSCRIPTCONTEXT()  # XSCRIPTCONTEXTの取得。
 XSCRIPTCONTEXT = automation()  # XSCRIPTCONTEXTを取得。 
 macro()  # マクロの実行。
グローバル変数XSCRIPTCONTEXTを使っているのは単にコードの使いまわしをしたかっただけという理由ですが、ドキュメントを取得している17行目以降のみにすればマクロでも実行できると思います(未確認)。

ディスクに保存していないドキュメントにアクセスするには、SimpleFileAccessのcopy()メソッドを使うこの方法が一番簡単です。

ディスクに保存しているドキュメントではvnd.sun.star.pkgプロトコールで同様にコピーできます(LibreOffice5(108)ドキュメントストレージからファイルシステムストレージにデータをコピーする の「LibreOfficeで開いていないドキュメント内へのURIを使う」参照。ただし制約があります。LibreOffice5(109)子要素にフォルダしか持たないドキュメント内フォルダ参照。)。

ストレージを取得して埋め込みマクロフォルダを取り出すスクリプト


ストレージは開いていないドキュメントからも開いているドキュメントからも取得できます。

この方法以降はフォルダをまるごと取り出すことはできず、各ファイルについてコピーしています。

ストレージは子要素名をキーとする辞書として扱えるはずですが、一階層ずつ子要素を取得しないとfor文でキーをイテレートできませんでした(LibreOffice 5.4.1.2で確認)。

ストレージの場合はフォルダとしてSimpleFileAccessのcreateFolder()メソッドで同名のフォルダを作成し、ストリームの場合はファイルとしてSimpleFileAccessのwriteFile()メソッドで同名ファイルに書き出しています。

このストリームはXStream型なのでgetInputStream()メソッドとgetOutputStream()メソッドをもっているのですが、サポートしているサービスもインターフェイスも無いようです。
import unohelper  # オートメーションには必須(必須なのはuno)。
import glob
import os
from com.sun.star.embed import ElementModes  # 定数
def macro():  
 ctx = XSCRIPTCONTEXT.getComponentContext()  # コンポーネントコンテクストの取得。
 smgr = ctx.getServiceManager()  # サービスマネージャーの取得。 
 simplefileaccess = smgr.createInstanceWithContext("com.sun.star.ucb.SimpleFileAccess", ctx)  # SimpleFileAccess
 os.chdir("..")  # 一つ上のディレクトリに移動。
 ods = glob.glob("*.ods")[0]  # odsファイルを取得。最初の一つのみ取得。
 systempath = os.path.join(os.getcwd(), ods)  # odsファイルのフルパス。
 doc_fileurl = unohelper.systemPathToFileUrl(systempath)  # fileurlに変換。
 desktop = ctx.getByName('/singletons/com.sun.star.frame.theDesktop')  # デスクトップの取得。
 components = desktop.getComponents()  # ロードしているコンポーネントコレクションを取得。
 for component in components:  # 各コンポーネントについて。
  if hasattr(component, "getURL"):  # スタートモジュールではgetURL()はないため。
   if component.getURL()==doc_fileurl:  # fileurlが一致するとき
    documentstorage = component.getDocumentStorage()  # コンポーネントからストレージを取得。
    break
 else:  # ドキュメントが開いていない時。
  storagefactory = smgr.createInstanceWithContext('com.sun.star.embed.StorageFactory', ctx)  # StorageFactory
  documentstorage = storagefactory.createInstanceWithArguments((doc_fileurl, ElementModes.READ))  # odsファイルからストレージを読み取り専用で取得。
 if not ("Scripts" in documentstorage and "python" in documentstorage["Scripts"]):
  print("The embedded macro folder does not exist in {}.".format(ods))
  return
 dest_dir = createDest(simplefileaccess)  # 出力先フォルダのfileurlを取得。
 scriptsstorage = documentstorage["Scripts"]  # documentstorage["Scripts"]["python"]ではイテレーターになれない。
 getContents(simplefileaccess, scriptsstorage["python"], dest_dir)  # 再帰的にストレージの内容を出力先フォルダに展開。
def getContents(simplefileaccess, storage, pwd):  # SimpleFileAccess、ストレージ、出力フォルダのfileurl 
 for name in storage:  # ストレージの各要素名について。
  fileurl = "/".join((pwd, name))  # 出力先fileurl。
  if storage.isStorageElement(name):  # ストレージのときはフォルダとして処理。
   if not simplefileaccess.exists(fileurl):  # 出力先フォルダが存在しない時は作成する。
    simplefileaccess.createFolder(fileurl)
   getContents(simplefileaccess, storage[name], fileurl)  # 子要素について同様にする。
  elif storage.isStreamElement(name):  # ストリームの時はファイルに書き出す。
   simplefileaccess.writeFile(fileurl, storage[name].getInputStream())  # ファイルが存在しなければ新規作成してくれる。
関数createDest()とif __name__ == "__main__"以降は最初のスクリプトと同じなので省略しています。

LibreOffice5(108)ドキュメントストレージからファイルシステムストレージにデータをコピーするではSimpleFileAccessのメソッドではなく、ファイルシステムストレージのストリームに直接ドキュメント内のストリームを渡しています。

ドキュメントを単なるzipファイルとして扱って埋め込みマクロフォルダを取り出すスクリプト

import glob
import os
from zipfile import ZipFile
import shutil
def macro():  # オートメーションでのみ実行可。 
 os.chdir("..")  # 一つ上のディレクトリに移動。
 ods = glob.glob("*.ods")[0]  # odsファイルを取得。最初の一つのみ取得。
 src_path = os.path.join(os.getcwd(), "src")  # srcフォルダのパスを取得。
 embeddemacro_path = "Scripts/python/"  # ドキュメント内のパス。
 output_path = "/".join((src_path, embeddemacro_path))  # 出力先フォルダのパス。src/Scripts/python/
 if os.path.exists(output_path):  # 出力先フォルダが存在する時。
  shutil.rmtree(output_path)  # 出力先フォルダを削除。
 with ZipFile(ods , 'r') as odszip: # odsファイルをZipFileで読み取り専用で開く。
  [odszip.extract(name, path=src_path) for name in odszip.namelist() if name.startswith(embeddemacro_path)]  # Scripts/python/から始まるパスのファイルのみ出力先フォルダに解凍する。
  print("Extract the embedded macro folder from {}.".format(ods))
if __name__ == "__main__":  # オートメーションで実行するとき 
 macro()
これは単なるzipファイル内の特定のフォルダを解凍しているだけで、UNO APIは使っていません。

ディスクに保存されたドキュメント内のフォルダを取り出すのはこの方法が一番簡単です。

ドキュメント内のすべてのファイルとフォルダについてforループすることになります。

パッケージを取得して埋め込みマクロフォルダを取り出すスクリプト


ストレージと違ってパッケージは一階層ずつ取得しなくても辞書として扱えました。

ただストレージと違ってディスクに保存されているドキュメントファイルからしかパッケージは取得できません。

つまりこの方法はzipコマンドで取り出すのと同じことになります。
import unohelper  # オートメーションには必須(必須なのはuno)。
import glob
import os
def macro():  
 ctx = XSCRIPTCONTEXT.getComponentContext()  # コンポーネントコンテクストの取得。
 smgr = ctx.getServiceManager()  # サービスマネージャーの取得。 
 simplefileaccess = smgr.createInstanceWithContext("com.sun.star.ucb.SimpleFileAccess", ctx)  # SimpleFileAccess
 os.chdir("..")  # 一つ上のディレクトリに移動。
 ods = glob.glob("*.ods")[0]  # odsファイルを取得。最初の一つのみ取得。
 systempath = os.path.join(os.getcwd(), ods)  # odsファイルのフルパス。
 doc_fileurl = unohelper.systemPathToFileUrl(systempath)  # fileurlに変換。
 package = smgr. createInstanceWithArgumentsAndContext("com.sun.star.packages.Package", (doc_fileurl,), ctx)  # Package。第2引数はinitialize()メソッドで後でも渡せる。
 docroot = package.getByHierarchicalName("/")  # /Scripts/pythonは不可。
 if not ("Scripts" in docroot and "python" in docroot["Scripts"]):
  print("The embedded macro folder does not exist in {}.".format(ods))
  return 
 python_fileurl = createDest(simplefileaccess)  # 出力先フォルダのfileurlを取得。
 getContents(simplefileaccess, docroot["Scripts"]["python"], python_fileurl) 
def getContents(simplefileaccess, folder, pwd):
 for sub in folder:  # 子要素のオブジェクトについて。
  name = sub.getName()  # 子要素のオブジェクトの名前を取得。
  fileurl = "/".join((pwd, name))  # 出力先のfileurlを取得。
  if sub.supportsService("com.sun.star.packages.PackageFolder"):  # PackageFolderの時はフォルダとして出力。
   if not simplefileaccess.exists(fileurl):
    simplefileaccess.createFolder(fileurl)
   getContents(simplefileaccess, sub, fileurl)  # 子要素のオブジェクトについて同様に出力。
  elif sub.supportsService("com.sun.star.packages.PackageStream"):  # PackageStreamのときはファイルとして出力。
   simplefileaccess.writeFile(fileurl, sub.getInputStream())  # ファイルが存在しなければ新規作成してくれる。
関数createDest()とif __name__ == "__main__"以降は最初のスクリプトと同じなので省略しています。

ZipFileAccessを取得して埋め込みマクロフォルダを取り出すスクリプト


これもパッケージと同様にディスクに保存されているドキュメントからしか取得できません。

ドキュメント内のすべてのフォルダとファイルがドキュメント直下のフォルダ名からフルパスで取得できます。
import unohelper  # オートメーションには必須(必須なのはuno)。
import glob
import os
def macro():  
 ctx = XSCRIPTCONTEXT.getComponentContext()  # コンポーネントコンテクストの取得。
 smgr = ctx.getServiceManager()  # サービスマネージャーの取得。 
 simplefileaccess = smgr.createInstanceWithContext("com.sun.star.ucb.SimpleFileAccess", ctx)  # SimpleFileAccess
 os.chdir("..")  # 一つ上のディレクトリに移動。
 ods = glob.glob("*.ods")[0]  # odsファイルを取得。最初の一つのみ取得。
 systempath = os.path.join(os.getcwd(), ods)  # odsファイルのフルパス。
 doc_fileurl = unohelper.systemPathToFileUrl(systempath)  # fileurlに変換。
 zipfileaccess = smgr. createInstanceWithArgumentsAndContext("com.sun.star.packages.zip.ZipFileAccess", (doc_fileurl,), ctx)  # ZipFileAccess。第2引数はinitialize()メソッドで後でも渡せる。
 python_fileurl = createDest(simplefileaccess)  # 出力先フォルダのfileurlを取得。
 macropath = "Scripts/python"  # 埋め込みマクロフォルダへのパス。
 for name in zipfileaccess:  # ドキュメント内のすべてのファイル名とフォルダ名がイテレートされる。最初に/はつかない。
  if macropath in name:  # 埋め込みマクロフォルダ下のみ。
   fileurl = "".join((python_fileurl, name.replace(macropath, "")))  # 出力fileurlを作成。
   if name.endswith("/"):  # フォルダの時。
    if not simplefileaccess.exists(fileurl):
     simplefileaccess.createFolder(fileurl)
   else:  # フォルダ以外の時はストリームを取得する。
    simplefileaccess.writeFile(fileurl, zipfileaccess.getStreamByPattern(name))  # ファイルが存在しなければ新規作成してくれる。  
関数createDest()とif __name__ == "__main__"以降は最初のスクリプトと同じなので省略しています。

ドキュメント内のフルパスが取得できるので再帰関数を使わずに出力できています。

その代わりドキュメント内のすべてのファイルとフォルダについてforループすることになります。

ZipFileAccessのサービスとインターフェイスの一覧

def macro():
 ctx = XSCRIPTCONTEXT.getComponentContext()  # コンポーネントコンテクストの取得。
 smgr = ctx.getServiceManager()  # サービスマネージャーの取得。 
 tcu = smgr.createInstanceWithContext("pq.Tcu", ctx)  # サービス名か実装名でインスタンス化。
 doc = XSCRIPTCONTEXT.getDocument()  # ドキュメント。
 zipfileaccess = smgr. createInstanceWithArgumentsAndContext("com.sun.star.packages.zip.ZipFileAccess", (doc.getURL(),), ctx)  # ZipFileAccess。第2引数はinitialize()メソッドで後でも渡せる。
 tcu.wtree(zipfileaccess)
ZipFileAccessはfileurlを渡さないとtcu.wtree()の引数にできませんでした。

ドキュメントのgetURL()メソッドでfileurlを渡しているのでディスク上に保存したドキュメントに対して実行しないといけません。


├─.packages.zip.ZipFileAccess
│   └─.packages.zip.XZipFileAccess2
│     ├─.container.XNameAccess
│     │   │        any  getByName( [in] string aName
│     │   │             ) raises ( .lang.WrappedTargetException,
│     │   │                        .container.NoSuchElementException)
│     │   │   [string]  getElementNames()
│     │   │    boolean  hasByName( [in] string aName)
│     │   └─.container.XElementAccess
│     │           type  getElementType()
│     │        boolean  hasElements()
│     └─.packages.zip.XZipFileAccess
│           .io.XInputStream  getStreamByPattern( [in] string aPattern
│                                      ) raises ( .packages.zip.ZipException,
│                                                 .packages.WrongPasswordException,
│                                                 .io.IOException,
│                                                 .container.NoSuchElementException)
├─.lang.XComponent
│     void  addEventListener( [in] .lang.XEventListener xListener)
│     void  dispose()
│     void  removeEventListener( [in] .lang.XEventListener aListener)
├─.lang.XInitialization
│     void  initialize( [in] [any] aArguments
│            ) raises ( .uno.Exception)
└─.comp.packages.zip.ZipFileAccess

参考にしたサイト


LibreOffice: TransientDocumentsContentProvider Service Reference
LibreOfficeで開いているドキュメントにアクセスするURLの解説。

OOoBasic/Generic/Desktop - ...?
開いているドキュメントモデルの取得方法。

OOoBasic/Generic/Zip - ...?
UNO APIでzipファイルを扱う方法。

次の関連記事:LibreOffice5(106)FileContentのサービスとインターフェイスの一覧

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ