ピボットテーブルを1つ含むODSワークブックがライブラリを通ると、ソースのセルを1つ編集しただけで、保存されたファイルはセル値はすべて無傷なのにピボットが消えて開きます。content.xmlは入力時に<table:data-pilot-table>を1つ持っていたのに、出力時にはゼロになっている。見かけ上の修復は、ピボットXMLを文字列としてコピーして渡すことです。要素数は戻ります。その直後、名前空間を理解するパーサーが未束縛プレフィックスエラーで保存されたパートを丸ごと拒否します。2つの失敗の根は1つです。XMLの断片は、依存する名前空間宣言が一緒に運ばれるときだけ、安全に運べるということ
ODSのピボット定義はcontent.xmlのどこにあるのか
OpenDocumentのピボットテーブル(フォーマット用語ではdata pilot)は、content.xml内でシートの後に格納される定義です。このパートを自前のオブジェクトモデルから再構築するライターは、モデルに置き場所があって初めてそれを保持します。ODF 1.3 Part 3 §9.6がコンテナを定めています。<table:data-pilot-tables>はピボットごとに1つの<table:data-pilot-table>を持ち、それぞれがtable:source-cell-range、table:data-pilot-field子要素の集合、table:target-range-address、table:buttonsリストを持ちます。ピボットが表示する結果セルはワークシート内に普通のセルとして存在します。だからセル、スタイル、入力規則、名前付き式、データベース範囲だけを再生成するライターは、画面の数字は同じでピボットのからくりが後ろにない、完全に妥当なスプレッドシートを作ります
HotXLS Delphi Excel ComponentはOpenODSで1つもセルをパースする前に、content.xmlから<table:data-pilot-tables>サブツリー全体を読み取り、それからそのコレクションをピボットモデルへの翻訳に試みます。モデルが表現できる素の形の定義、つまりローカルなソース範囲、行フィールド、集計メジャー、ソースデータと整合する保存済み結果を持つものは、ワークシートのPivotTablesリストのTXLSPivotTableになります。XLSXピボットから得られるのと同じオブジェクトで、Delphiから計算済みフィールドを追加したりリフレッシュしたりできます。その形から外れるものはすべて、取り込んだXMLのままコレクションに保持され、SaveAsODSがそれを書き戻します。翻訳器が知らない名前空間の属性が1つあるだけで十分です。LibreOfficeが保存したピボットはloext:やcalcext:属性を運ぶことがあり、loext:属性が1つあれば定義はキャプチャパスに回されます
フォールバックはワークブック単位のオール・オア・ナッシングです。3つのピボットのうち1つでも翻訳可能な形から外れると、3つとも取り込んだXMLのまま残ります。分割するとcontent.xmlに1つのコンテナに対する2つの真実の源ができ、片半はオブジェクトから、もう片半はテキストから再構成され、両者の食い違いはそのままユーザーのファイルに着地します。コレクションを丸ごと保つのが安全なデフォルトで、これはHotXLSがXLSXの未知のextLstブロックに適用してきたのと同じ方針です(未知のextLstブロックのロスレス往返)。モデルが運べないものは、書かれたまま保持します
切り出したピボット定義はなぜ保存ファイルを壊すのか
文字列検索でcontent.xmlから切り出したピボット定義は、プレフィックスに意味を与える名前空間宣言を失います。Namespaces in XMLは宣言のないプレフィックスを整形式エラーとして扱います。文字列アプローチは短いコードです。パートをWideStringにデコードし、Posで<table:data-pilot-tablesを探し、その後に対応する閉じタグを探し、間を全部コピーする。出てくるテキストは完全に見えます。ページフィールドにloext:ignore-selected-page="true"が、全レベルにcalcext:repeat-item-labels="false"が入っています。しかし、これらのプレフィックスを束縛するxmlns:loextとxmlns:calcextの宣言は、何千文字も手前の文書ルート<office:document-content>の上に置かれたままです
Namespaces in XML 1.0 §6.1は、宣言をその開始タグから対応する終了タグまでのスコープに置き、内側のプレフィックス付き名はすべてそれに対して解決します。文書からサブツリーを切り出すと、そのスコープからも切り出されます。断片を新しい文書に貼ったとき、各プレフィックスが解決されるのは、新しいルートがたまたまそれを宣言している場合だけです。HotXLSは自前の<office:document-content>を、ライターが使う名前空間(office、table、text、style、fo、draw、calcext、tableooo、ofなど)とともに書きます。切り出したコピーではtable:は解決し、calcext:も解決しますが、loext:は解決しません。未束縛のプレフィックスが1つあるだけで、名前空間を理解するパーサーはパート全体を拒否します。ピボットが静かに劣化してくれるようなことはなく、周囲のシートデータまで一緒に読めなくなります
文字列検索には、リーダーベースの取り込みでは絶対に会わない別の死角もあります。Posはコメントの中、CDATAセクションの中、そのテキストを引用する属性値の中のタグテキストにも一致します。同名のネストした要素を区別できません。しかもリテラルのプレフィックスtable:に一致しますが、ODFはプロデューサーがtable名前空間を好きなプレフィックスに束縛することを許しています
HotXLSは名前空間スコープを断片にどう載せるのか
HotXLSは、宣言が現れた深さとともにすべての名前空間宣言を追跡するストリーミングXMLリーダーでピボットサブツリーを取り込み、ターゲット要素が到達した瞬間、まだ効いている宣言を断片の開始タグにコピーします。スタックはxmlnsやxmlns:prefixを宣言する開始タグごとに伸び、束縛を宣言した要素が閉じると縮みます。空要素も同じ扱いが必要です。<x xmlns:p="..."/>は別個の終了タグイベントを決して出さないので、ポップは空の開始タグでも発火させないと、束縛が兄弟要素に漏れ出します
ターゲット地点では、取り込みはスタックを最も内側の束縛から外側へ歩き、各プレフィックスを1回ずつ書きます。内側から外側へ歩くのがシャドーイングの実装です。<office:spreadsheet>がルートがすでに宣言したプレフィックスを束縛し直しているなら、ピボット地点で効いているのは近いほうの値です。§6.1が要求するとおりの挙動で、すでに書いたプレフィックスの集合が外側の値の追従を防ぎます。ピボット要素自身が宣言するプレフィックスはスキップします。1つのタグにxmlns:tableを2回書くのは、これも別の整形式エラーだからです
ターゲットは名前空間で照合されるので、tをODFのtable URIに束縛した<t:data-pilot-tables>を書くプロデューサーでも認識されます。取り込まれたタグのつづりはファイルのままです。空白テキストノードは書かれたとおりに戻ります。属性の引用符の選択と<x/>か<x></x>かの選択は正規化されます。断片はバイト単位のコピーではなくリーダーイベントから再シリアライズされるからです。ピボットサブツリーが開いたままcontent.xmlが終わる場合(切り詰められたファイルで起こります)、オープンは例外で失敗します。次の保存時に書き戻される半分の定義を残すより、こちらが良い
運ぶ宣言集合は意図的に寛容です。取り込みはスコープ内のすべての束縛をコピーします。サブツリーが使うかどうかは問いません。だから断片のルートは、保存された文書のルートが宣言しているにもかかわらず、xmlns:tableやxmlns:calcextを繰り返します。ネストしたスコープでのプレフィックス再宣言はNamespaces in XMLの下で合法で、ルートに何十もの名前空間を宣言するプロデューサーのファイルでも、数キロバイトが増えるだけです。リストを刈り込むには、要素名、属性名、QNameを保持する属性値に現れるすべてのプレフィックスをサブツリーから精査する必要があり、そこで1つ見逃せば未束縛プレフィックスの失敗がすぐに戻ってきます。冗長な宣言数個のコストは、そのリスクよりはるかに安い
ピボットを運ぶワークブックのオープン、編集、保存
呼び出し側で切り替えるものは何もありません。ピボット定義は普通のOpenODSとSaveAsODSに乗って運ばれます。ワークシートは1から番号が付き、両メソッドとも成功で1を返します:
uses
System.SysUtils, lxHandleX;
procedure EditPivotSource(const InFile, OutFile: string);
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.OpenODS(InFile) <> 1 then
raise Exception.CreateFmt('Cannot open %s', [InFile]);
// E2はSheet1.A2:E30というピボットソース範囲の内側にある
Book.Sheets[1].Cells[2, 5].Value := 1250.0;
if Book.SaveAsODS(OutFile) <> 1 then
raise Exception.CreateFmt('Cannot save %s', [OutFile]);
finally
Book.Free;
end;
end;
保存されたcontent.xmlでは、ピボットコレクションは<office:spreadsheet>の中、ライターが生成する<table:named-expressions>の直後、<table:database-ranges>の前に置かれます。AutoFilter(HotXLSはデータベース範囲として書きます)とピボットの両方を運ぶワークブックでは、この2つの要素が隣り合います。パイプラインで厳密なODFスキーマバリデーターを回しているなら、まずここを見ます
この例の編集はソースの値を変えるだけで、ピボットの結果セルはそのままです。取り込まれた定義は書かれたとおりに再生されます。HotXLSは再計算せず、ファイルを開く表計算アプリケーションが次のリフレッシュでピボットを再計算します。アドレスも同じです。Sheet1をリネームしたりソースブロックを移動したりすると、保存された定義はまだSheet1.A2:E30を指し、それを消費するアプリケーションはリフレッシュ時に壊れた範囲として報告します。ピボットをこの方法で運ぶワークブックでは、シート名とソースブロックを安定させておいてください
あるピボットがどちらのパスを通ったかを知るには
いちばん手っ取り早い確認はワークシートのPivotTablesリストです。翻訳された定義はここにTXLSPivotTableとして現れ、取り込まれた定義は現れません。変換レポートは疑問の後半、別の出力フォーマットでピボットに何が起こるか、に答えます。GetConversionReportは保存も再計算もせずにワークブックを検査し、返されたレポートの所有権は呼び出し側にあります:
uses
System.SysUtils, lxHandleX, lxDiagnostics;
function DescribeOdsPivots(Book: TXLSXWorkbook): string;
var
I, Modeled: Integer;
Report: TXLSConversionReport;
begin
Modeled := 0;
for I := 1 to Book.Sheets.Count do // Sheetsは1始まり
Inc(Modeled, Book.Sheets[I].PivotTables.Count);
Report := Book.GetConversionReport(xlsConversionOpenXmlWorkbook);
try
for I := 0 to Report.Count - 1 do
if (Report[I].Feature = 'OpenDocumentPivotTables') and
(Report[I].Fidelity = xlsConversionDropped) then
Exit('Captured ODF pivot XML: keep this workbook on .ods');
finally
Report.Free;
end;
if Modeled > 0 then
Result := Format('%d pivot table(s) in the pivot model', [Modeled])
else
Result := 'No pivot definitions';
end;
出力パスが重要なのは、取り込んだXMLがODFの語彙で、それを出力するのはODSライターだけだからです。.xlsxで保存すると、取り込まれたコレクションにはSpreadsheetMLの対応物がなく、レポートはそれをOpenDocumentPivotTablesの下にxlsConversionDroppedで列挙します。バイナリの.xlsbライターは、それをまだ保持するワークブックの保存を拒否します。翻訳されたピボットは普通のモデルオブジェクトで、代わりにターゲットフォーマットのルールに従います。ODSインポートパスとエクスポートパスの違いはここでもほかと同様に効きます。ピボットを運ぶファイルの安全な経路は、.ods入力、.ods出力です
オール・オア・ナッシングのフォールバックからもう1つのルールが従います。ワークブックは取り込んだコレクションを再生するか、モデルからピボットを書くかのどちらかで、1つのcontent.xmlに2つを混ぜることは決してありません。取り込んだピボットXMLをすでに保持するワークブックにAddPivotTableでピボットを追加すると、ODSの保存は生成XMLを再生XMLの隣に継ぎ合わせる代わりに、エラーで停止します
保存パートを2つ目のパーサーで確認する
要素数は名前空間について何も証明しません。保存されたcontent.xmlからdata-pilot-tableの開始タグを見つける正規表現が見るのはテキストで、第2節の切り出しコピーはそのテストを通過してしまいます。ファイルを書いたライブラリとは無関係の、名前空間を理解するパーサーははるかに厳しい審判で、Delphiには1つ最初から入っています。下の関数は自分のテストスイートに追加して使うためのチェックです。TZipFileとTXMLDocumentはHotXLSではなくDelphi RTL由来です:
uses
System.SysUtils, System.Classes, System.Zip,
Xml.xmldom, Xml.XMLIntf, Xml.XMLDoc;
// 自分のテストヘルパー:.odsパッケージ内のcontent.xmlが
// 名前空間を理解するDOMパーサーで読み込めるときTrue
function OdsContentParses(const OdsFile: string; out Error: string): Boolean;
var
Zip: TZipFile;
Bytes: TBytes;
Stream: TBytesStream;
Doc: IXMLDocument;
begin
Error := '';
Zip := TZipFile.Create;
try
Zip.Open(OdsFile, zmRead);
Zip.Read('content.xml', Bytes);
finally
Zip.Free;
end;
Stream := TBytesStream.Create(Bytes);
try
Doc := TXMLDocument.Create(nil);
try
Doc.LoadFromStream(Stream); // 未束縛のプレフィックスはここで失敗する
Result := True;
except
on E: EDOMParseError do
begin
Error := E.Message;
Result := False;
end;
end;
finally
Stream.Free;
end;
end;
コードが保存するすべての.odsでこれを回し、さらにピボットを運ぶファイルでは一歩進んで、パース後の入力と出力のピボットサブツリーを比較します。タグ、ソート済み属性、テキスト、子要素を再帰的に、名前は名前空間URIとローカル名に展開して比較します。この比較なら、つづり替えられたプレフィックスは通し、未束縛のものは捕まえます。無害と壊れているの境界線はまさにそこです。ユーザーが実際に使うすべてのプロデューサーのファイルを喰わせてください。table名前空間を別のプレフィックスに束縛する文書、ピボット要素自身にプレフィックスを宣言する文書、<office:spreadsheet>で束縛し直す文書は、単一のLibreOfficeファイルでは鍛えられないスキップとシャドーイングのルールを鍛えてくれます
ODSリーダーとライター、XLSXとODS両方のピボットモデル、上で使った変換レポートは、すべてHotXLS Delphi Excel Componentの一部です。製品ページでは、DelphiとC++Builder向けのXLSX、XLS、ODSの完全な読み書き対応を説明しています