Technischer Artikel

ODS-Dateien in Delphi mit HotXLS öffnen und speichern

Ein Delphi-Berichts-Backend, das seit Jahren .xlsx ausgibt, bekommt eine neue Anforderung: Die Beschaffungsregeln eines Kunden aus dem öffentlichen Sektor schreiben OpenDocument-Spreadsheet-Ausgabe vor, und die Analysten auf diesem Konto schicken ihre Bearbeitungen als aus LibreOffice gespeicherte .ods-Dateien zurück. Nun muss derselbe Code also ODS schreiben und lesen. HotXLS, die native Object-Pascal-Tabellenkalkulationsbibliothek von losLab für Delphi und C++Builder, beherrscht beide Richtungen, ohne dass irgendwo Excel oder LibreOffice installiert ist. Die beiden Richtungen sind nah beieinander, aber nicht identisch. Seit HotXLS 2.88.229 bringt der Importpfad Formeln als lebendige Formeln neben ihren zwischengespeicherten Ergebnissen zurück, und aktuelle Versionen lesen auch Stile, Zahlenformate, verbundene Bereiche, Kommentare, Bilder, Diagramme und bedingte Formate. Was nicht überlebt, sind die volle OpenFormula-Semantik und eine Handvoll Details auf Paketebene, sodass ein Team, das ein perfektes Spiegelbild annimmt, trotzdem auf Randfälle trifft, irgendwo zwischen der Revision des Kunden und dem nächsten Bericht, meist ohne einen Fehler, auf den man zeigen könnte

Die ODS-Unterstützung lebt auf der XLSX-Fassade, nicht auf der XLS-Fassade

HotXLS liefert zwei unabhängige Klassenhierarchien in einem Paket: TXLSWorkbook in der Unit lxHandle für binäre BIFF8-.xls-Dateien und TXLSXWorkbook in der Unit lxHandleX für OOXML-.xlsx-Pakete. Jeder OpenDocument-Einstiegspunkt - OpenODS, SaveAsODS, GetODSSheetNames - hängt an TXLSXWorkbook. Die Platzierung ist nicht willkürlich. Ein ODS-Paket, wie in OASIS ODF 1.3 spezifiziert, ist ein Zip-Archiv mit einem mimetype-Mitglied, einem Manifest und einem content.xml-Rumpf, was es zu einem strukturellen Verwandten des OOXML-Zips macht; BIFF8 ist ein binärer Record-Stream aus den 1990ern ohne jede Gemeinsamkeit

Diese Platzierung hat eine praktische Kante: Eine alte .xls-Arbeitsmappe kann nicht mit einem Aufruf zu .ods werden. Man überführt den BIFF-Inhalt zuerst mit SaveXLSWorkbookAsXLSX aus der Unit lxXlsxExport in das XLSX-Modell, öffnet das Ergebnis über TXLSXWorkbook erneut und exportiert von dort. Die Brücke ist nicht verlustfrei, und man sollte die Lücken kennen, bevor man darauf aufbaut. Sie kopiert Werte, Formeln, Zahlenformate, Schriftarten, Füllungen, Rahmen, verbundene Bereiche, Spaltenbreiten und Zeilenhöhen. Sie verwirft Kommentare, Diagramme und bedingte Formatierung. Eine .xls-Quelle mit starker Formatierung erreicht ODS schlichter, als sie losgegangen ist, und das ist eine Eigenschaft der Brücke, nicht des ODS-Writers

Die Erkennung auf der Importseite ist automatisch. Die einfache Methode Open erkennt ein ODS-Paket an seinem mimetype-Mitglied und fällt auf eine Prüfung des obersten content.xml zurück, wenn dieses Mitglied fehlt, sodass ein generischer Codepfad „öffne, was auch immer der Nutzer hochgeladen hat" kein eigenes Erweiterungsschnüffeln braucht. Nach dem Öffnen meldet die Eigenschaft SourceFormat, welcher Zweig gegriffen hat

Diagramm des HotXLS-Klassenlayouts in Delphi, bei dem jeder ODS-Einstiegspunkt auf TXLSXWorkbook lebt und eine SaveXLSWorkbookAsXLSX-Brücke BIFF8-.xls-Inhalt hinüberträgt
Jeder OpenDocument-Einstiegspunkt hängt an TXLSXWorkbook, und eine alte .xls-Datei erreicht ODS nur über die verlustbehaftete Brücke von BIFF nach XLSX

Export nach ODS mit TODSExportOptions

Der Exportaufruf selbst ist eine Zeile; das Optionsobjekt darum herum trägt die Entscheidungen, nach denen ein Prüfer später fragen wird:

var
  Book: TXLSXWorkbook;
  Opts: TODSExportOptions;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    Opts := TODSExportOptions.Create;        // der Aufrufer besitzt und gibt dies frei
    try
      Opts.Generator := 'ReportService 4.2'; // meta:generator überschreiben
      Opts.IncludeCharts := True;
      Opts.IncludeImages := True;
      Book.SaveAsODS('quarterly-report.ods', Opts);
    finally
      Opts.Free;
    end;
  finally
    Book.Free;
  end;
end;

Das Optionsobjekt gehört dem Aufrufer. HotXLS gibt es nicht frei, weshalb das innere try..finally dort steht und nicht optional ist. Die zwei Eigenschaften, die die Ausgabe verändern, statt sie nur zu beschriften, verdienen einen genaueren Blick. IncludeCharts := False zu setzen tut mehr, als Diagramme zu verbergen: Es entfernt die Diagramm-Unterdokumente und ihre Manifest-Einträge aus dem Paket, was genau das ist, was man will, wenn der Abnehmer eine Datenpipeline ist, die darüber stolpern würde. Generator überschreibt den ODF-String meta:generator, der sonst HotXLS/<version> lautet; man überschreibt ihn, wenn nachgelagerte Werkzeuge Dateiproduzenten per Fingerabdruck erkennen, um Support zu routen. Trifft nichts davon zu, lässt man das Optionsobjekt ganz weg. Der Aufruf SaveAs(FileName, xlsxOpenDocumentSpreadsheet) ist dasselbe wie SaveAsODS mit Standardwerten, und Stream-Überladungen auf beiden lassen Sie das Paket ohne temporäre Datei direkt in eine HTTP-Antwort schreiben

Was der Importpfad liest - und was er noch überspringt

Lesen Sie diesen Teil sorgfältig, bevor Sie jemandem Round-Trip-Treue versprechen. Der ODS-Import in HotXLS begann als leichtgewichtiger Pfad, der nur skalare Werte und zwischengespeicherte Formelergebnisse behielt, und er ist weit über dieses Stadium hinausgewachsen. Aktuelle Versionen lesen content.xml zusammen mit styles.xml und settings.xml: skalare Werte und zwischengespeicherte Formelergebnisse, wiederholte Zeilen und Spalten, ins Raster expandiert, verbundene Bereiche, Rich Text, Hyperlinks, Kommentare, eingebettete Bilder und Diagramme, vererbte und Standard-Zellstile, Zahlen- und Datumsformate, Zeilenhöhen und Spaltenbreiten, benannte Bereiche, Fixierungen (Freeze Panes), AutoFilter-Bereiche, Datenvalidierungen und bedingte Formate. Was OpenODS noch nicht konvertiert, sind die volle OpenFormula-Semantik, ODS-Schutzschlüssel, Kommentar-Geometrie, erweiterte Kommentarformatierung und Paketfeatures außerhalb des Tabelleninhalts

Formeln sind der Teil, der am ehesten zubeißt. Eine ODF-Zelle speichert zwei Dinge nebeneinander: den Formelausdruck, geschrieben im OpenFormula-Dialekt aus ODF 1.3 Teil 4, und den letzten Wert, den die erzeugende Anwendung dafür berechnet hat. Frühe Versionen lasen nur den zwischengespeicherten Wert. Seit HotXLS 2.88.229 importiert OpenODS auch den Ausdruck: Eine Zelle mit dem Präfix of:= oder oooc:= behält ihr zwischengespeichertes Ergebnis und bekommt eine normalisierte Excel-Formel, wobei eingeklammerte Referenzen wie [.A1:.B1] und Semikolon-Argumenttrenner in Excel-Syntax konvertiert werden. Seit 2.384.32 konvertiert der Import auch Inline-Array-Zeilen ({1;2|3;4} wird zu {1,2;3,4}) und den Referenz-Vereinigungsoperator ~, und seit 2.384.55 liest eine solche Vereinigung ohne das zweite Klammerpaar zurück. OpenFormula in Excel-Syntax zu übersetzen bleibt ein Dialektkonvertierungsproblem mit echten Randfällen bei Funktionsvokabularen, Referenzsyntax und Fehlermodellen — daher reist der zwischengespeicherte Wert mit: Die importierten Zahlen sind exakt die Zahlen, die der Absender zuletzt gesehen hat

Der Fehlermodus, um den man herum entwerfen muss, hat sich entsprechend verengt. Vor 2.88.229 importierte eine Tabelle, deren Summen korrekt waren, als LibreOffice sie zuletzt gespeichert hat, korrekte Zahlen, die jetzt Konstanten waren: Eingabezelle bearbeitet, neu berechnet, nichts bewegte sich. Auf aktuellen Versionen kommen die Formeln lebendig zurück, also bleibt als Restrisiko ein Konstrukt, das der Übersetzer nicht modelliert, was eine Formel hinterlassen kann, die sich anders neu berechnet als in der erzeugenden Anwendung, während der zwischengespeicherte Wert weiterhin das Ergebnis des Absenders zeigt. Vergleichen Sie neu berechnete Ergebnisse mit den importierten zwischengespeicherten Werten für die Formeln, von denen Ihre Pipeline abhängt, und stellen Sie jede abweichende Formel aus Ihren eigenen Geschäftsregeln über Cell.Formula wieder her, das auf der XLSX-Fassade den Ausdruck ohne führendes Gleichheitszeichen entgegennimmt

Um die verbleibenden Round-Trip-Lücken herum entwerfen

Der Export rendert aus dem vollständigen Arbeitsmappenmodell im Speicher: Werte, Formeln, Stile und, wenn man sie anfordert, Diagramme und Bilder. Der Formelexport wurde von Version zu Version nachgeschärft. Seit 2.384.55 behalten Inline-Arrays, Referenzvereinigungen und absolute Referenzen ihre Bedeutung in OpenFormula; seit 2.384.56 deklariert HotXLS die Namespaces seiner Formelpräfixe, sodass LibreOffice die Formeln auswertet, statt in jeder Formelzelle #VALUE! anzuzeigen; und seit 2.384.65 werden Ganze-Spalte- und Ganze-Zeile-Referenzen in der OpenFormula-Form [.A:.A] geschrieben, sodass Excel sie nicht mehr als #NAME? öffnet. Die bedingten Formate folgten: Seit 2.384.66 werden formelbasierte Regeln als formula-is(...) mit ihrer Basiszelle geschrieben, was LibreOffice erkennt, und seit 2.384.69 werden Wert- und Formelregeln zusätzlich als standardmäßige style:map-Bedingungen geschrieben, die einzige Form, die Excel liest, während die Importseite beide Formen liest. Ein vollständiger Zyklus .xlsx nach .ods nach .xlsx behält auf einer aktuellen Version also Werte, Formeln, Stile und bedingte Formate; was er noch verlieren kann, liegt in den Lücken aus dem vorigen Abschnitt

Diagramm des HotXLS-ODS-Round-Trips aus Delphi: Export aus dem Arbeitsmappenmodell im Speicher und ein Import, den frühe Versionen auf Werte beschränkten, sodass Formeln vor HotXLS 2.88.229 als Konstanten blieben
Frühe Versionen importierten nur Werte und zwischengespeicherte Ergebnisse, sodass ein vollständiger Zyklus .xlsx nach .ods nach .xlsx Stile und lebendige Formeln fallen ließ; aktuelle Versionen lesen beides zurück
Book := TXLSXWorkbook.Create;
try
  Book.Open('vendor-revision.ods');          // Format wird automatisch erkannt
  if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
  begin
    // Seit HotXLS 2.88.229 kommen Formeln lebendig neben ihren
    // zwischengespeicherten Ergebnissen zurück; stellen Sie jede Formel wieder her,
    // der die nachgelagerte Pipeline als übersetzt nicht vertraut, bevor Sie speichern.
    Book.Sheets[0].Cells[2, 5].Formula := 'SUM(B2:D2)';
    Book.SaveAs('vendor-revision.xlsx');
  end;
finally
  Book.Free;
end;

Das Architekturmuster, das daraus folgt: Eingehende .ods-Dateien als Datenfeeds behandeln, nicht als Dokumente, die man an Ort und Stelle bearbeitet. Die kanonische Arbeitsmappe in .xlsx halten, Werte aus Kundenrevisionen auslesen und bei Bedarf frisches ODS aus der kanonischen Kopie ausgeben. Die Verifikation gehört in beide Lager - exportierte Dateien in LibreOffice Calc öffnen, dem Referenz-ODF-Konsumenten, und in Excel, das ODS seit Jahren liest, aber an den Rändern der Diagramm- und Stilunterstützung von LibreOffice abweicht. Blattanzahl, eine Handvoll Schlüsselzellen und das Vorhandensein von Diagrammen ergeben eine ausreichende Smoke-Prüfung je Exportprofil

Eine ODS-Datei vor dem Import triagieren

Wenn ein Endpunkt Uploads annimmt, ist das Auflisten der Blattnamen weit günstiger als ein vollständiges Parsen und fängt strukturelle Überraschungen früh ab:

Diagramm des HotXLS-Upload-Triage-Tors in Delphi, bei dem GetODSSheetNames unlesbare ODS-Pakete und fehlende Blätter zurückweist, bevor ein vollständiger Import läuft
Eine GetODSSheetNames-Sondierung kostet weit weniger als ein vollständiges Parsen und fängt den Fehler des umbenannten Blatts ab, solange die Fehlermeldung noch die Datei benennen kann
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetODSSheetNames('incoming.ods', Names) <= 0 then
    raise Exception.Create('not a readable ODS package');
  if Names.IndexOf('Data') < 0 then
    raise Exception.Create('revision is missing the Data sheet');
finally
  Book.Free;
  Names.Free;
end;

Die Rückgabekonvention bringt Leute zu Fall: HotXLS-Aufrufe geben im Allgemeinen bei Erfolg eine positive Anzahl oder 1 zurück und bei Fehler -1, wobei sie die Liste beim Scheitern leeren, daher sollte auf <= 0 geprüft werden, statt mit einem bestimmten positiven Wert zu vergleichen. GetODSSheetNames setzt die Workbook-Instanz weder zurück noch befüllt es sie, sodass ein einzelnes Sondierungsobjekt ein ganzes Verzeichnis eingehender Dateien prüfen kann. Strukturelle Prüfungen wie diese fangen den häufigsten Fehler aus der Praxis - ein Analyst benennt ein Blatt um oder löscht es, bevor er die Revision zurückschickt - am Tor ab, wo die Fehlermeldung noch die Datei und das fehlende Blatt benennen kann, statt drei Ebenen tiefer als nil-Referenz aufzutauchen

Wenn Sie darum herum eine breitere Konvertierungspipeline bauen, zeigt das Muster der Arbeitsmappen-Audit- und Konvertierungswerkbank, wie man die Merkmale einer Datei inventarisiert, bevor man ein Zielformat wählt, und der Leitfaden zur Leistung großer Arbeitsmappen hält Batch-Exporte in vernünftigen Speichergrenzen

HotXLS ist eine native Tabellenkalkulationsbibliothek für Delphi und C++Builder mit vollständigem Quellcode; die komplette Funktionsliste und die Lizenzdetails finden sich auf der Produktseite der HotXLS Delphi Component