2019年11月17日日曜日

20: LibreOfficeをファイルコンバータとして最適に用いる(C#実装)

<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>

OpenDocument/Microsoft Office/等フォーマット間で、PDF/CSVフォーマットへ、高速かつ万能に、ドキュメントを調整することも可能。動かせるサンプルあり。

話題


About: UNO (Universal Network Objects)
About: LibreOffice
About: Apache OpenOffice
About: C#

この記事の目次


開始コンテキスト



ターゲットコンテキスト



  • 読者は、C#にてUNO APIを直接に用いてファイルコンバージョンを実装する方法を理解し、動かせるサンプルを得る。

オリエンテーション


本シリーズの任意のサンプルプログラムをビルドする方法の記事があります。

LibreOfficeまたはApache OpenOfficeデーモンを作成する方法の記事があります。

LibreOfficeまたはApache OpenOffice Microsoft Windowsサービスを作成する方法の記事があります。

オフィスファイルをオープニングパスワードでUNOを用いて暗号化する方法の記事があります。

OpenDocumentファイルに編集パスワードをUNOを用いてセットすることについての記事があります。

バイナリWord/Excelファイルに編集パスワードをUNOを用いてセットすることについての記事があります。

Office Open XMLファイルに編集パスワードをUNOを用いてセットすることについての記事があります。

ワードプロセッサドキュメントのページのサイズをUNOを用いてセットすることについての記事があります。

スプレッドシートドキュメントのページのサイズをUNOを用いてセットすることについての記事があります。

ワードプロセッサドキュメントの任意のページプロパティをUNOを用いてセットすることについての記事があります。

スプレッドシートドキュメントの任意のページプロパティをUNOを用いてセットすることについての記事があります。

任意のオフィスドキュメントをPDFファイルとしてフル仕様指定でUNOを用いてエクスポートすることについての記事があります。

任意のスプレッドシートをCSVへ任意のフォーマットでUNOを用いて書き出すことについての記事があります。

任意のスプレッドシートドキュメントの全スプレッドシートをCSVファイル群へUNOを用いて書き出すことについての記事があります。


本体

ト書き
Special-Student-7、Tony(C#プログラマー )、Abby(C#プログラマー)がコンピュータの前にいる。


0: 注意: 'soffice --convert-to' は決して使いません


Special-Student-7
本記事のコンセプトパートが読まれているものと想定されていることは明確に述べられているものの、読者の内のかなりの割合が読んでいないし読まないだろうことが推測でき、多くの人々が'soffice --convert-to'を使うことに固執している事実を鑑みて、念押しすることが適切だろうと感じて申しますが、ここでは、'soffice --convert-to'は決して使われません。

その理由は、コンセプトパートに詳述されています。


1: UNO APIを用いたファイルコンバージョンロジックの実装


Special-Student-7
知っておく必要のあることは本記事のコンセプトパートから既にご存知であると想定して、UNO APIを用いたファイルコンバージョンロジックのC#実装をすぐに見ましょう、ここで、'l_underlyingRemoteUnoObjectsContextInXComponentContext'はLibreOfficeまたはApache OpenOfficeインスタンスへのUNOオブジェクト群コンテキスト、'l_originalFileUrl'は元ファイルのURL、'l_targetFileUrl'はターゲットファイルのURL、'l_fileStoringFilterName'はファイル格納フィルター名です。

@C# ソースコード
namespace theBiasPlanet {
	namespace unoUtilitiesTests {
		namespace fileConvertingTest1 {
			using System;
			using System.Text.RegularExpressions;
			using unoidl.com.sun.star.beans;
			using unoidl.com.sun.star.frame;
			using unoidl.com.sun.star.uno;
			using unoidl.com.sun.star.util;
			~
			
			public class Test1Test {
				private static readonly Regex c_urlRegularExpression;
				
				static Test1Test () {
					c_urlRegularExpression = new Regex ("(.*:)(.*)");
				}
				
				// the 'com.sun.star.util.URLTransformer' UNO service can be used if you want to, as I do this manually.
				private static unoidl.com.sun.star.util.URL getUrlInURL (String a_url) {
					unoidl.com.sun.star.util.URL l_urlInURL = new unoidl.com.sun.star.util.URL ();
					l_urlInURL.Complete = a_url;
					l_urlInURL.Main = l_urlInURL.Complete;
					MatchCollection l_regularExpressionMatches = c_urlRegularExpression.Matches (l_urlInURL.Complete);
					foreach (Match l_regularExpressionMatch in l_regularExpressionMatches) {
						l_urlInURL.Protocol = l_regularExpressionMatch.Groups [1].Value;
						l_urlInURL.User = "";
						l_urlInURL.Password = "";
						l_urlInURL.Server = "";
						l_urlInURL.Port = 0;
						l_urlInURL.Path = l_regularExpressionMatch.Groups [2].Value;
						l_urlInURL.Name = "";
						l_urlInURL.Arguments = "";
						l_urlInURL.Mark = "";
						break;
					}
					return l_urlInURL;
				}
				
				public void main (String [] a_argumentsArray) {
					~
							String l_originalFileUrl = a_argumentsArray [2];
							String l_targetFileUrl = a_argumentsArray [3];
							String l_fileStoringFilterName = a_argumentsArray [4];
							
							XDispatchProvider l_underlyingUnoDesktopInXDispatchProvider = (XDispatchProvider) (l_underlyingRemoteUnoObjectsContextInXComponentContext.getValueByName ("/singletons/com.sun.star.frame.theDesktop").Value);
							XSynchronousDispatch l_underlyingFileOpeningUnoDispatcherInXSynchronousDispatch = (XSynchronousDispatch) (l_underlyingUnoDesktopInXDispatchProvider.queryDispatch (getUrlInURL ("file:///"), "_blank", -1));
							
							unoidl.com.sun.star.util.URL l_originalFileUrlInURL = getUrlInURL (l_originalFileUrl);
							PropertyValue [] l_unoDocumentOpeningPropertiesArray = new PropertyValue [4];
							l_unoDocumentOpeningPropertiesArray [0] = new PropertyValue ("ReadOnly", -1, new uno.Any (true), PropertyState.DIRECT_VALUE);
							l_unoDocumentOpeningPropertiesArray [1] = new PropertyValue ("Hidden", -1, new uno.Any (true), PropertyState.DIRECT_VALUE);
							l_unoDocumentOpeningPropertiesArray [2] = new PropertyValue ("OpenNewView", -1, new uno.Any (true), PropertyState.DIRECT_VALUE);
							l_unoDocumentOpeningPropertiesArray [3] = new PropertyValue ("Silent", -1, new uno.Any (true), PropertyState.DIRECT_VALUE);
							PropertyValue [] l_unoDocumentStoringPropertiesArray = new PropertyValue [2];
							l_unoDocumentStoringPropertiesArray [0] = new PropertyValue ("FilterName", -1, new uno.Any (l_fileStoringFilterName), PropertyState.DIRECT_VALUE);
							l_unoDocumentStoringPropertiesArray [1] = new PropertyValue ("Overwrite", -1, new uno.Any (true), PropertyState.DIRECT_VALUE);
							uno.Any l_underlyingOriginalUnoDocumentInAny = (uno.Any) (l_underlyingFileOpeningUnoDispatcherInXSynchronousDispatch.dispatchWithReturnValue (l_originalFileUrlInURL, l_unoDocumentOpeningPropertiesArray));
							Boolean l_hasSucceeded = false;
							if (l_underlyingOriginalUnoDocumentInAny.hasValue ()) {
								XStorable2 l_underlyingOriginalUnoDocumentInXStorable2 = (XStorable2) (l_underlyingOriginalUnoDocumentInAny.Value);
								try {
									l_underlyingOriginalUnoDocumentInXStorable2.storeToURL (l_targetFileUrl, l_unoDocumentStoringPropertiesArray);
									l_hasSucceeded = true;
								}
								catch (System.Exception l_exception) {
									throw l_exception;
								}
								( (XCloseable) l_underlyingOriginalUnoDocumentInXStorable2).close (false); 
							}
							else {
								Console.Out.WriteLine (String.Format ("### The original file: '{0:s}' cannot be opened.", l_originalFileUrl));
								Console.Out.Flush ();
							}
					~
				}
			}
		}
	}
}


Tony
. . . ふーむ、十分にシンプルだな。俺が変えないといけないのは、それらURLとそのフィルター名の設定だけだな?

Special-Student-7
最小要件のためには、はい、既に、UNOオブジェクト群コンテキストを取得済みであるとしてですが、勿論(もしもそうでなければ、ある記事(単にまたは用意周到な方法用)にしたがってそうすることができます)。

Tony
"最小要件のためには"は、ペテンに聞こえるな。本体を安めに買わせておいて、高いオプションを売り始める。

Special-Student-7
私は何も売っていないでしょう、サー?

Tony
どうかな!

Special-Student-7
とにかく、上記コードは、まさに、本記事のコンセプトパートのあるセクションにて申し上げたことを行なっています: 元ファイルをオープンする、オープンされたドキュメントをターゲットフォーマットにて格納する、オープンされたドキュメントをクローズする。

多分ご推測できるとおり、'dispatchWithReturnValue'が元ファイルをオープンして、'storeToURL'がオープンされたドキュメントを格納して、'close'がオープンされたドキュメントをクローズしています。

'l_underlyingFileOpeningUnoDispatcherInXSynchronousDispatch'は、あるUNOディスパッチャであり、ファイル毎に取得する必要はありません。

Tony
どんなフィルター名を使えるんだ?

Special-Student-7
それは、本記事のコンセプトパートに挙げられています。


2: オープンするまたは格納することに対するプロパティ群について


Special-Student-7
上記コードに見られるとおり、ファイルをオープンすることに対していくつかのプロパティをセットすることができます。実のところ、以下が、可能なプロパティ群の仕様です、ここで、データタイプはC#タイプです(UNOタイプではなく)。注意として、各データは、実際には、'uno.Any'インスタンスでラップしなければなりません。

名前データタイプ説明
ReadOnlyBooleanオープンされたドキュメントを元ファイルに書き戻せないかどうか: 'true'-> できない、'false'-> できる
HiddenBooleanオープンされたドキュメントは不可視かどうか: 'true'-> 不可視、'false'-> 不可視でない
OpenNewViewBooleanドキュメントは新たなビュー内にオープンされるかどうか: 'true'-> 新たなビュー内に、'false'-> 新たなビュー内にではない
SilentBooleanドキュメントはサイレントにオープンされるかどうか: 'true'-> サイレントに、'false'-> サイレントにではない
PasswordStringオープニングパスワード

関心を持たれるかもしれないプロパティに、オープニングパスワードがあります。

Abby
それじゃあ、私は、オープニングパスワードでプロテクトされたファイルをコンバートできるのね。

Special-Student-7
はい、マダム。

ご注意いただきたいのですが、UNO 'string'は公式には'null'を受け入れません、したがって、もしもオープニングパスワードがなければ、プロパティ自体、指定すべきではありません、'null'が偶然受け入れられることはあるかもしれませんが。

これもご注意いただきたいのですが、'ReadOnly'は、オープンされたドキュメントが編集できないことを意味せず、そのドキュメントは元ファイルへ書き戻せないことを意味します、したがって、ドキュメントが調整されて新たなファイルへ書かれるのに、それが'false'である必要はありません。

ドキュメントを格納することに対しても、いくつかのプロパティをセットすることができます。実のところ、以下が、可能なプロパティ群の仕様です、ここで、データタイプはC#タイプです(UNOタイプではなく)。注意として、各データは、実際には、'uno.Any'インスタンスでラップしなければなりません。

名前データタイプ説明
FilterNameStringthe filter name フィルター名
FilterData場合による一部のフィルターに対するフィルター特有のデータ: 常にというわけではないが、しばしば'unoidl.com.sun.star.beans.PropertyValue'の配列
FilterOptionsString一部のフィルターに対するフィルター特有のデータ
AsTemplateBooleanドキュメントがテンプレートとして格納されるかどうか: 'true'-> テンプレートとして、'false'-> テンプレートとしてではない
AuthorString著者名
DocumentTitleStringドキュメントタイトル
EncryptionDataunoidl.com.sun.star.beans.NamedValue []一部のフィルターに対する暗号化データ
PasswordString一部のフィルターに対するオープニングパスワード
CharacterSetString文字セット
VersionInt16バージョン番号
CommentStringバージョン記述
OverwriteBooleanファイルが上書きされるかどうか: 'true' -> 上書きされる、'false' -> 上書きされない
ComponentData場合による一部のフィルター特有データ: 常にというわけではないが、しばしば'unoidl.com.sun.star.beans.NamedValue'の配列
ModifyPasswordInfo場合による一部のフィルターに対する編集パスワードデータ

オープニングパスワードは実のところ暗号化データに他なりません。なぜ、'EncryptionData'と'Password'の両方があるかという理由は、一部のフィルターは一方を使い、他の一部のフィルターは他方を使うことです。詳細は、別の記事をご参照ください。

Abby
えーと、ただ1つの編集パスワードプロパティしかないようだけど、あなたの説明は"一部のフィルターに対する"と言ってる。他のフィルターはどうなるの?

Special-Student-7
編集パスワードをセットすることに関する事情はかなり複雑です: 編集パスワードは、フォーマット特有のアルゴリズムでハッシュ化されなければならず、ハッシュは、フォーマット特有の場所へセットされなければならず、場所は必ずしもファイル格納プロパティ群の中ではありません。 . . . 詳細は、OpenDocumentファイル用Microsoftバイナリファイル用Office Open XMLファイル用のある記事に説明されています。

Abby
えーと . . .

Tony
具体的に何が'FilterData'や'FilterOptions'といった、それら"フィルター特有"プロパティにセットできるんだ?

Special-Student-7
それは、ファイルコンバージョンの一般的ロジックのためのものである本記事では説明できません: PDFファイル用CSVファイル用といった、いくつかの他の記事をご参照ください。 .


3: ドキュメントを調整することについて


Special-Student-7
勿論、ターゲットファイルは、ファイル格納プロパティ群を介してのみでは満足のいくようにコントロールされないかもしれません。

その場合は、オープンされたドキュメントを調整することができます。

Tony
. . . どうやって?

Special-Student-7
上記コードにおいて、'l_underlyingOriginalUnoDocumentInXStorable2'は、オープンされたドキュメントのUNOオブジェクトを代表しており、それを介して、オープンされたドキュメントを調整することができます。

Tony
. . . どうやって?

Special-Student-7
それは勿論ドキュメント特有、要件特有であって、いくつかの他の記事、例えば、ワードプロセッサドキュメントのページプロパティ群を調整するためのもの(ここおよびここ)やスプレッドシートドキュメントのページプロパティ群を調整するためのもの(ここおよびここ)をご参照ください。


4: 動かせるサンプルがここにあります


Special-Student-7
ある動かせるサンプルがここからダウンロード可能です。

そのサンプルプログラムは、引数の1つとして、ファイルコンバージョン命令群を格納したJSONファイルのパス(実のところ、複数のJSONファイルパスを取れます)を取り、それらの命令を実現します。

JSONファイルの仕様は以下のとおりです。

@JSON ソースコード
[
	{Disabled: %本レコードが無効化されているか否か -> 'true'または'false'%, 
	OriginalFileUrl: %元ファイルURL、その中では任意のオペレーティングシステム環境変数を使用できる、'%{HOME}'のように%, 
	TargetFileUrl: %ターゲットファイルURL、その中では任意のオペレーティングシステム環境変数を使用できる、'%{HOME}'のように%, 
	FileStoringFilterName: %ファイル格納フィルター名%, 
	OpeningPassword: %元ファイルに対するオープニングパスワード%, 
	Title: %ターゲットファイルタイトル%, 
	EncryptingPassword: %ターゲットファイルに対する暗号化パスワード%, 
	EditingPassword: %ターゲットファイルに対する編集パスワード%, 
	CertificateIssuerName: %ターゲットファイルに署名するための証明書発行者名%, 
	CertificatePassword: %ターゲットファイルに署名するための証明書パスワード%, 
	SignatureTimeStampAuthorityUrl: %ターゲットファイルに署名するためのタイムスタンプオーソリティURL%, 
	DocumentTailorNames: [%ドキュメントテイラー名%, . . .], 
	AllSpreadSheetsAreExported: %全スプレッドシートがエクスポートされるか否か: 'true'または'false'%, 
	TargetFileNamingRule: %ターゲットCSVファイル群に対する命名規則: '0'-> 各CSVファイルはシートインデックスを使って命名される、'1'-> 各CSVファイルはシート名を使って命名される%, 
	CsvItemsDelimiterCharacterCode: %CSV項目間デリミタの文字コード: 例えば、'44'-> ','%, 
	CsvTextItemQuotationCharacterCode: %CSVテキスト項目クォーテーションの文字コード: 例えば、'34'-> '"'%, 
	CsvCharactersEncodingCode: %CSV文字群エンコーディングコード: '76' -> UTF-8、'65535' -> UCS-2、'65534' -> UCS-4、'11' -> US-ASCII、'69' -> EUC_JP、'64' -> SHIFT_JIS%, 
	CsvAllTextItemsAreQuoted: %全てのCSVテキスト項目がクウォートされるか否か: 'true'または'false'%, 
	CsvContentsAreExportedAsShown: %CSVコンテンツがシート上に表示されているとおりにエクスポートされるか否か: 'true'または'false'%, 
	CsvFormulaeThemselvesAreExported: %シートセルフォーミュラ自体がCSVファイルへエクスポートされるか否か: 'true'または'false'%, 
	HiddenSpreadSheetsAreExported: %非表示スプレッドシートもエクスポートされるか否か%}, 
	. . .
	{. . .}
]

Abby
"テイラー"?

Special-Student-7
"テイラー"は、ドキュメントを特定の方法にて調整する任務を帯びたオブジェクトです。

Abby
そのテイラーはどこから来たの?

Special-Student-7
私はいくつかのサンプルテイラーを用意しましたが、特定のニーズをお持ちでしたら、ご自分でお作りになる必要があるでしょう。

Abby
えーと、クラス名を指定するってこと?

Special-Student-7
いいえ、オブジェクト名です: 1つのクラス由来の複数のテイラーがあるかもしれません、それぞれ違うパラメータでコンストラクトされた。

Tony
俺の命令はWebリクエストとして来るんだ、したがって、そのサンプルを、Webリクエストを受け付けるように変更しなければならない . . .

Special-Student-7
各命令をメモリ内JSONレコードに構成すれば、そのJSONレコードを、'executeConversionOrder'メソッドに渡すことができます。

Tony
何だ、その . . . 何とかメソッドてのは?

Special-Student-7
そのサンプルにはそういうメソッドがあります。

サンプルは複数の命令群ファイルを受け付けるようにしてあり、それらのファイルは同時実行で処理されます、その目的は、この最適な方法では、コンバージョンは同時実行で行なえることをデモンストレーションすることです。


4-1: サンプルプロジェクトをビルドする


Special-Student-7
サンプルプロジェクトをビルドする方法は、ある記事に説明されています。.

特に、注意すべきは、プロジェクト群は、いくつか別のプログラミング言語による実装も含んでおり、それらは、C#プログラマーであるあなたは多分必要としないだろうということです。そうした不要なコードを無視する方法はそこに説明されています。

メインプロジェクトは'filesConverter'です。


4-2: サンプルプログラムを実行する


Special-Student-7
サンプルプログラムを実行する前に、LibreOfficeまたはApache OpenOfficeインスタンスを、それがクライアントからのコネクションを受け付けるように開始しておかなければなりません、ある以前の記事にて学んだ方法によって。

ト書き
Special-Student-7は、LibreOfficeインスタンスをポート番号2002で開始する。

Special-Student-7
コンバージョン命令群JSONファイルが必要です(上に挙げられた仕様で)で、私たちは、アーカイブファイルに'filesConverter/data/FileConversionOrders.json'として含まれているサンプルJSONファイルを使用しますが、少なくとも、その中のファイルURL群は調整しなければなりません。

サンプルプログラムは以下のように実行できます、カレントディレクトリをサンプルプロジェクトディレクトリに位置させて。

@cmd ソースコード
gradle i_executeCsharpExecutableFileTask -Pi_commandLineArguments="\"socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext\" \"data/FileConversionOrders.json\""

ト書き
Special-Student-7は、上記コマンドを、ターミナル内でカレントディレクトリをサンプルプロジェクトディレクトリに位置させて実行する。

Special-Student-7
もしも、LibreOfficeまたはApache OpenOfficeインスタンスがリモートホストにいるのであれば、ホスト名は'localhost'から変えればよいだけです、もしも、ファイアウォールが通信をブロックしないのであればですが、勿論。

そして、以下のように複数のJSONファイルを指定することができます。

@cmd ソースコード
gradle i_executeCsharpExecutableFileTask -Pi_commandLineArguments="\"socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext\" \"data/FileConversionOrders.json\" \"data/FileConversionOrders2.json\""


4-3: サンプルコードを理解する


Special-Student-7
注意として、サンプルコードは私のユーティリティプロジェクト('coreUtilitiesToBeDisclosed'および'unoUtilitiesToBeDisclosed')を使っており、それらは、直接にはファイルコンバージョンの作業に関係していないコードを含んでいます。それらユーティリティプロジェクトの無関係な部分のみを取り去るのはそれほど容易でないことをご理解ください、コードは関連しあっているので。

ファイルコンバージョンロジックは、'theBiasPlanet.unoUtilities.filesConverting.FilesConverter'クラスの'convertFile'メソッド(CSVファイルへコンバートするためのものを除き)または'convertSpreadSheetsDocumentFileToCsvFiles'メソッド(CSVファイルへコンバートするためのもの)内にあり、メソッドは、ドキュメントテイラー群とファイル格納プロパティ群を取ります、いくつか他のものに加えて。

新たなドキュメントテイラーをインストールするためには、当該クラスを'theBiasPlanet.unoUtilities.documentsHandling.UnoDocumentTailor'の子として、そのクラスのインスタンスを一意な名前で、'theBiasPlanet.filesConverter.programs.FilesConverterConsoleProgram'クラスの'l_unoDocumentTailorNameToUnoDocumentTailorMap'変数へ登録しなければなりません。


参考資料


<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>