Een ODS-workbook met één draaitabel gaat door een library, er wordt één broncel bewerkt, en het opgeslagen bestand opent met alle celwaarden intact en de draaitabel verdwenen: content.xml bevatte bij het openen één <table:data-pilot-table> en bij het opslaan nul meer. De voor de hand liggende reparatie is de draaitabel-XML als string kopiëren. Dat herstelt het elementaantal, en daarna weigert een namespace-bewuste parser het opgeslagen onderdeel ronduit met een fout over een ongebonden prefix. Beide storingen hebben één wortel: een stuk XML reist alleen veilig als de namespacedeclaraties waarvan hij afhangt met hem meereizen
Waar een ODS-draaitabeldefinitie in content.xml woont
Een OpenDocument-draaitabel, die het formaat een data pilot noemt, is een definitie die in content.xml na de werkbladen wordt bewaard, en een schrijver die dat onderdeel uit zijn eigen objectmodel herbouwt, houdt hem alleen over als het model ergens te plaatsen heeft. ODF 1.3 Part 3 §9.6 beschrijft de container: <table:data-pilot-tables> bevat één <table:data-pilot-table> per draaitabel, elk met een table:source-cell-range, een set table:data-pilot-field-kinderen, een table:target-range-address en een lijst table:buttons. De resultaatcellen die de draaitabel toont, zitten in het werkblad als gewone cellen. Een schrijver die alleen cellen, stijlen, validaties, named expressions en database ranges opnieuw genereert, levert dus een prima geldige spreadsheet op met dezelfde cijfers op het scherm en geen enkele draaitabel erachter
HotXLS Delphi Excel Component leest bij OpenODS, nog vóór er één cel is geparseerd, de hele subboom <table:data-pilot-tables> uit content.xml en probeert de verzameling vervolgens naar zijn pivotmodel te vertalen. Een definitie in de kale vorm die het model representeert, met een lokale bronrange, een rijveld, aggregaatmaten en opgeslagen resultaten die kloppen met de brongegevens, wordt een TXLSPivotTable in de lijst PivotTables van het werkblad, hetzelfde object dat u van een XLSX-draaitabel krijgt en dat u met berekende velden kunt uitbreiden en vanuit Delphi kunt verversen. Alles wat buiten die vorm valt, houdt de verzameling als vastgelegde XML, en SaveAsODS speelt die terug. Eén attribuut in een namespace die de vertaler niet kent, is genoeg: draaitabellen die LibreOffice opslaat kunnen loext:- en calcext:-attributen meedragen, en een loext:-attribuut stuurt de definitie het capture-pad op
De fallback is alles of niets per workbook. Valt één draaitabel in een verzameling van drie buiten de vertaalbare vorm, dan blijven alle drie als vastgelegde XML staan. Ze opsplitsen zou content.xml met twee bronnen van waarheid voor één container achterlaten, de helft herbouwd uit objecten en de helft teruggespeeld uit tekst, en elke tegenstrijdigheid tussen beide helften zou in het bestand van de gebruiker belanden. De verzameling heel houden is de veiligere default, en dat is dezelfde politiek die HotXLS toepast op onbekende extLst-blokken in XLSX: wat het model niet kan dragen, blijft staan zoals het geschreven was
Waarom breekt een uitgesneden draaitabeldefinitie het opgeslagen bestand?
Een draaitabeldefinitie die met een stringzoekactie uit content.xml wordt geknipt, verliest de namespacedeclaraties die zijn prefixes betekenis geven, en Namespaces in XML behandelt een prefix zonder declaratie als een welgevormdheidsfout. De stringaanpak is kort: decodeer het onderdeel naar een WideString, vind <table:data-pilot-tables met Pos, vind daarna de bijbehorende sluittag, en kopieer alles daartussen. De tekst die eruit komt oogt compleet. Ze bevat loext:ignore-selected-page="true" op een paginaveld en calcext:repeat-item-labels="false" op elk niveau, maar de declaraties xmlns:loext en xmlns:calcext die die prefixes binden, staan op de wortel van het document, <office:document-content>, duizenden tekens eerder
Namespaces in XML 1.0 §6.1 houdt een declaratie in scope van de begintag die hem draagt tot de bijbehorende eindtag, en elke naam met een prefix binnen die scope lost ertegen op. Knip een subboom uit het document en u knipt hem uit die scope. Wordt het fragment in een nieuw document geplakt, dan lost elke prefix alleen op als de nieuwe wortel hem toevallig declareert. HotXLS schrijft zijn eigen <office:document-content> met de namespaces die zijn schrijver gebruikt, waaronder office, table, text, style, fo, draw, calcext, tableooo en of. In de uitgesneden kopie lost table: op, lost calcext: op, en loext: niet. Eén ongebonden prefix is genoeg voor een namespace-bewuste parser om het hele onderdeel te weigeren, dus de draaitabel degradeert niet gracieus; ook de bladgegevens eromheen worden onleesbaar
Stringzoeken heeft nog andere blinde vlekken waar een capture via een reader nooit mee te maken krijgt. Pos matcht de tagtekst binnen een commentaar, binnen een CDATA-sectie, of binnen een attribuutwaarde die haar aanhaalt. Hij kan geneste elementen met dezelfde naam niet uit elkaar houden. En hij matcht de letterlijke prefix table:, terwijl ODF een producent toestaat de tablenamespace aan elke prefix te binden die hij kiest
Hoe HotXLS de namespacescope op het fragment overdraagt
HotXLS legt de draaitabelsubboom vast met een streaming XML-reader die elke namespacedeclaratie bijhoudt samen met de diepte waarop hij verscheen, en kopieert op het moment dat het doelelement aankomt de nog geldende declaraties op de openingstag van het fragment. De stapel groeit bij elke begintag die xmlns of xmlns:prefix declareert en krimpt zodra het element dat een binding declareerde sluit. Lege elementen hebben dezelfde behandeling nodig: <x xmlns:p="..."/> levert nooit een apart eindtag-event op, dus de pop moet ook op de lege begintag afgaan, anders lekt een binding naar zijn siblings
Bij het doel loopt de capture de stapel van de binnenste binding naar buiten en schrijft elke prefix één keer. Van binnen naar buiten lopen is precies wat shadowing implementeert. Bindt <office:spreadsheet> een prefix die de wortel al declareerde, dan is de dichterbij liggende waarde degene die bij de draaitabel geldt, precies zoals §6.1 vereist, en een set al geschreven prefixes houdt de buitenste waarde tegen. Een prefix die het draaitabelelement op zichzelf declareert, wordt overgeslagen, want xmlns:table twee keer op één tag schrijven is een andere welgevormdheidsfout
Het doel wordt op namespace gematcht, dus een producent die <t:data-pilot-tables> schrijft met t gebonden aan de ODF-table-URI wordt nog steeds herkend, terwijl de vastgelegde tags de spelling uit het bestand houden. Whitespace-tekstknopen komen exact terug zoals ze geschreven zijn. Aanhalingstekens van attributen en de keuze tussen <x/> en <x></x> worden genormaliseerd, want het fragment wordt opnieuw geserialiseerd uit reader-events in plaats van byte voor byte gekopieerd. Eindigt content.xml terwijl de draaitabelsubboom nog open staat, zoals in een afgebroken bestand, dan faalt het openen met een exception in plaats van half een definitie vast te houden die bij de volgende opslag teruggeschreven zou worden
De overgedragen set is met opzet ruim. De capture kopieert elke binding in scope, of de subboom hem nu gebruikt of niet, dus de wortel van het fragment herhaalt xmlns:table en xmlns:calcext ook al declareert de wortel van het opgeslagen document ze ook. Een prefix in een geneste scope opnieuw declareren is legaal onder Namespaces in XML, en een bestand van een producent die tientallen namespaces op zijn wortel declareert, komt een paar kilobyte duurder uit. De lijst snoeien zou betekenen dat de subboom gescand wordt op elke prefix in elementnamen, attribuutnamen en attribuutwaarden die QNames bevatten, en één enkele misser daar haalt de ongebonden-prefix-fout meteen terug. Een paar redundante declaraties kosten veel minder dan dat risico
Een workbook met een draaitabel openen, bewerken en opslaan
Vanuit de aanroeper is er niets aan te zetten: de draaitabeldefinitie lift mee met een gewone OpenODS en SaveAsODS. Werkbladen worden vanaf 1 genummerd, en beide methodes geven 1 terug bij succes:
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 ligt binnen een bronrange van de draaitabel, 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;
In de opgeslagen content.xml zit de draaitabelverzameling binnen <office:spreadsheet> direct na de <table:named-expressions> die de schrijver genereert en vóór <table:database-ranges>. Een workbook die zowel een AutoFilter draagt, dat HotXLS als database range wegschrijft, als een draaitabel, zet de twee elementen naast elkaar, en als uw pipeline een strenge ODF-schemavalidator draait, is dat paar de plek om als eerste te bekijken
De bewerking in dat voorbeeld verandert een bronwaarde en laat de resultaatcellen van de draaitabel zoals ze waren. Een vastgelegde definitie wordt teruggespeeld zoals geschreven: HotXLS berekent haar niet opnieuw, en de spreadsheetapplicatie die het bestand opent, herberekent de draaitabel bij zijn volgende verversing. Hetzelfde geldt voor haar adressen. Hernoem Sheet1 of verplaats het bronblok, en de opgeslagen definitie wijst nog steeds naar Sheet1.A2:E30, wat de consumerende applicatie bij het verversen als kapot bereik meldt. Houd bladnamen en het bronblok stabiel in elke workbook waarvan de draaitabellen zo reizen
Welk pad volgde een gegeven draaitabel?
De snelste controle is de lijst PivotTables van het werkblad: een vertaalde definitie staat daar als TXLSPivotTable, een vastgelegde niet. Het conversierapport beantwoordt de andere helft van de vraag, wat er met de draaitabel in een ander uitvoerformaat zou gebeuren. GetConversionReport bekijkt de workbook zonder haar op te slaan of te herberekenen, en de aanroeper is eigenaar van het teruggegeven rapport:
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 is 1-based
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;
Het uitvoerpad telt mee, want vastgelegde XML is ODF-vocabulaire en alleen de ODS-schrijver schrijft die. Opgeslagen als .xlsx heeft de vastgelegde verzameling geen SpreadsheetML-tegenhanger, en het rapport zet haar onder OpenDocumentPivotTables met xlsConversionDropped. De binaire .xlsb-schrijver weigert een workbook die haar nog bevat. Een vertaalde draaitabel is een gewoon modelobject en volgt in plaats daarvan de regels van het doelformaat. De verschillen tussen de ODS-import- en exportpaden gelden hier net als overal, en voor bestanden met draaitabellen is de veilige route .ods erin, .ods eruit
Uit de alles-of-niets-fallback volgt nog een regel. Een workbook speelt ofwel een vastgelegde verzameling terug, ofwel schrijft zijn draaitabellen uit het model; hij mengt de twee nooit in één content.xml. Een draaitabel met AddPivotTable toevoegen aan een workbook die al vastgelegde draaitabel-XML bevat, laat de ODS-opslag met een fout stoppen in plaats van gegenereerde XML naast teruggespeelde XML te plakken
Het opgeslagen onderdeel controleren met een tweede parser
Een elementtelling bewijst niets over namespaces. Een reguliere expressie die begintags van data-pilot-table vindt in de opgeslagen content.xml ziet tekst, en de uitgesneden kopie uit de tweede sectie slaagt voor die test. Een namespace-bewuste parser die niets te maken heeft met de library die het bestand schreef, is een veel strengere rechter, en Delphi stuurt er standaard één mee. De functie hieronder is een controle om aan uw eigen testsuite toe te voegen; TZipFile en TXMLDocument komen uit de Delphi-RTL, niet uit HotXLS:
uses
System.SysUtils, System.Classes, System.Zip,
Xml.xmldom, Xml.XMLIntf, Xml.XMLDoc;
// Eigen testhulp: True als content.xml uit een .ods-pakket
// laadt in een namespace-bewuste DOM-parser
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); // een ongebonden prefix faalt hier
Result := True;
except
on E: EDOMParseError do
begin
Error := E.Message;
Result := False;
end;
end;
finally
Stream.Free;
end;
end;
Draai die controle op elke .ods die uw code opslaat, en ga voor bestanden met draaitabellen een stap verder door na het parsen de draaitabelsubboom van invoer en uitvoer te vergelijken: tag, gesorteerde attributen, tekst en kinderen, recursief, met namen uitgevouwen naar namespace-URI plus lokale naam. Die vergelijking laat een anders gespelde prefix door en vangt een ongebonden, en dat is precies de grens tussen onschadelijk en kapot. Voer haar bestanden van elke producent die uw gebruikers echt draaien. Een document dat de tablenamespace aan een andere prefix bindt, een prefix op het draaitabelelement zelf declareert, of er één in <office:spreadsheet> opnieuw bindt, oefent de regels voor overslaan en shadowing uit op een manier die één LibreOffice-bestand niet doet
De ODS-lezer en -schrijver, het pivotmodel voor zowel XLSX als ODS en het conversierapport van hierboven maken allemaal deel uit van de HotXLS Delphi Excel Component; de productpagina dekt de volledige XLSX-, XLS- en ODS-lees-en-schrijfondersteuning voor Delphi en C++Builder