Artykuł techniczny

Otwieranie i zapis plików ODS w Delphi z HotXLS

Backend raportowy w Delphi, który od lat emituje .xlsx, dostaje nowe wymaganie: reguły zamówień publicznych klienta z sektora publicznego nakazują wyjście w formacie OpenDocument Spreadsheet, a analitycy na tym koncie odsyłają swoje poprawki jako pliki .ods zapisane z LibreOffice. Więc teraz ten sam kod musi zapisywać ODS i go czytać. HotXLS, natywna biblioteka arkuszowa losLab w Object Pascalu dla Delphi i C++Buildera, obsługuje oba kierunki bez Excela ani LibreOffice zainstalowanego gdziekolwiek. Czego nie robi, to uczynienia obu kierunków symetrycznymi. Eksport niesie znacznie więcej, niż odzyskuje import, a zespół, który założy inaczej, będzie patrzył, jak formuły i formatowanie wyparowują gdzieś między poprawką klienta a następnym raportem, bez żadnego błędu, na który dałoby się wskazać

Wsparcie ODS mieszka na fasadzie XLSX, nie na XLS

HotXLS wysyła dwie niezależne hierarchie klas w jednym pakiecie: TXLSWorkbook w jednostce lxHandle dla binarnych plików BIFF8 .xls i TXLSXWorkbook w jednostce lxHandleX dla pakietów OOXML .xlsx. Każdy punkt wejścia OpenDocument - OpenODS, SaveAsODS, GetODSSheetNames - wisi na TXLSXWorkbook. To umieszczenie nie jest przypadkowe. Pakiet ODS, zgodnie ze specyfikacją OASIS ODF 1.3, to archiwum zip niosące człon mimetype, manifest i ciało content.xml, co czyni go strukturalnym kuzynem zipa OOXML; BIFF8 to binarny strumień rekordów z lat dziewięćdziesiątych, niemający z tym nic wspólnego

To umieszczenie ma praktyczną krawędź: starszy skoroszyt .xls nie stanie się .ods jednym wywołaniem. Najpierw przerzucasz treść BIFF do modelu XLSX, przez SaveXLSWorkbookAsXLSX z jednostki lxXlsxExport, otwierasz wynik ponownie przez TXLSXWorkbook, a potem eksportujesz stamtąd. Pomost nie jest bezstratny i warto znać luki, zanim na nim zbudujesz. Kopiuje wartości, formuły, formaty liczbowe, czcionki, wypełnienia i szerokości kolumn. Porzuca obramowania, scalone zakresy, komentarze, wykresy i formatowanie warunkowe. Źródło .xls z ciężkim formatowaniem dotrze do ODS wyglądając skromniej, niż wyruszyło, a to właściwość pomostu, nie zapisującego ODS

Wykrywanie po stronie importu jest automatyczne. Zwykła metoda Open rozpoznaje pakiet ODS po jego członie mimetype, wracając do sprawdzenia content.xml na najwyższym poziomie, gdy tego członu brak, więc ogólna ścieżka kodu w rodzaju "otwórz cokolwiek wrzucił użytkownik" nie potrzebuje własnego węszenia po rozszerzeniach. Po otwarciu właściwość SourceFormat raportuje, która gałąź odpaliła

Diagram układu klas HotXLS w Delphi, gdzie każdy punkt wejścia ODS mieszka na TXLSXWorkbook, a pomost SaveXLSWorkbookAsXLSX przenosi treść BIFF8 .xls na drugą stronę
Każdy punkt wejścia OpenDocument wisi na TXLSXWorkbook, a starszy .xls dociera do ODS tylko przez stratny pomost z BIFF do XLSX

Eksport do ODS przez TODSExportOptions

Samo wywołanie eksportu to jedna linia; obiekt opcji wokół niego niesie decyzje, o które recenzent zapyta później:

var
  Book: TXLSXWorkbook;
  Opts: TODSExportOptions;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    Opts := TODSExportOptions.Create;        // wołający jest właścicielem i to zwalnia
    try
      Opts.Generator := 'ReportService 4.2'; // nadpisanie meta:generator
      Opts.IncludeCharts := True;
      Opts.IncludeImages := True;
      Book.SaveAsODS('quarterly-report.ods', Opts);
    finally
      Opts.Free;
    end;
  finally
    Book.Free;
  end;
end;

Obiekt opcji należy do wołającego. HotXLS go nie zwolni, i dlatego wewnętrzne try..finally tam jest i nie jest opcjonalne. Dwie właściwości, które zmieniają wyjście, a nie tylko je etykietują, zasługują na bliższe spojrzenie. Ustawienie IncludeCharts := False robi więcej niż ukrycie wykresów: usuwa poddokumenty wykresów i ich wpisy w manifeście z pakietu, czyli dokładnie to, czego chcesz, gdy konsumentem jest potok danych, który by się o nie potknął. Generator nadpisuje ciąg meta:generator z ODF, który inaczej brzmi HotXLS/<version>; nadpisz go, gdy narzędzia niżej w łańcuchu identyfikują producentów plików, by kierować wsparcie. Jeśli nic z tego nie dotyczy, pomiń obiekt opcji w całości. Wywołanie SaveAs(FileName, xlsxOpenDocumentSpreadsheet) jest tym samym co SaveAsODS z ustawieniami domyślnymi, a przeciążenia strumieniowe na obu pozwalają zapisać pakiet prosto do odpowiedzi HTTP bez pliku tymczasowego

Co czyta ścieżka importu i co celowo pomija

Przeczytaj tę część uważnie, zanim obiecasz komukolwiek wierność podróży w obie strony. Import ODS w HotXLS jest celowo lekką ścieżką. Zachowuje skalarne wartości komórek i buforowany wynik, który każda formuła niosła w chwili zapisu, i rozwija powtarzane wiersze oraz kolumny do siatki. Nie przenosi stylów, wyrażeń formuł ODS ani rysunków

Wybór dotyczący formuł najpewniej ugryzie i został dokonany celowo. Komórka ODF przechowuje obok siebie dwie rzeczy: wyrażenie formuły, zapisane w dialekcie OpenFormula zdefiniowanym w ODF 1.3 część 4, i ostatnią wartość, jaką aplikacja produkująca dla niej policzyła. Tłumaczenie OpenFormula na składnię formuł Excela to osobny problem konwersji dialektów, z prawdziwymi przypadkami brzegowymi wokół słowników funkcji, składni referencji i modeli błędów. Odczyt wartości buforowanej zamiast tego omija całą tę klasę cichych błędów tłumaczenia, więc liczby, które importujesz, są dokładnie tymi, które nadawca widział ostatnio. Kosztem jest to, że przychodzą jako liczby, a nie jako żywe formuły, które je wytworzyły

Tryb awarii, wokół którego trzeba projektować, wynika wprost: arkusz, którego sumy były poprawne, gdy LibreOffice ostatnio go zapisał, importuje się z poprawnymi liczbami, ale te liczby są teraz stałymi. Zmień komórkę wejściową, przelicz i nic się nie ruszy - formuły nie ma, został tylko jej końcowy wynik. Jeśli przepływ pracy potrzebuje żywych formuł po imporcie, odtwórz je programowo z własnych reguł biznesowych przez Cell.Formula, które na fasadzie XLSX bierze wyrażenie bez wiodącego znaku równości

Projektowanie wokół asymetrycznej podróży w obie strony

Eksport renderuje z pełnego modelu skoroszytu w pamięci: wartości, style oraz, jeśli o nie poprosisz, wykresy i obrazy. Import zwraca same wartości. Więc odcinek z .xlsx do .ods ma wysoką wierność, a odcinek z .ods do .xlsx przynosi wartości i buforowane wyniki, ale żadnego stylowania i żadnych żywych formuł. Połącz oba, a asymetria się kumuluje. Pełny cykl .xlsx do .ods do .xlsx zapisuje wszystko wiernie w drodze na zewnątrz i gubi style oraz formuły w drodze z powrotem, choć na żadnym z kroków nic nie poszło źle

Diagram asymetrycznej podróży ODS w obie strony w HotXLS z Delphi: eksport o pełnej wierności z modelu skoroszytu w pamięci i import zwracający same wartości, który zostawia formuły jako stałe
Eksport renderuje pełny model z pamięci, a import zwraca wartości i buforowane wyniki, więc pełny cykl .xlsx do .ods do .xlsx po cichu gubi style i żywe formuły
Book := TXLSXWorkbook.Create;
try
  Book.Open('vendor-revision.ods');          // format wykrywany automatycznie
  if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
  begin
    // Wartości i buforowane wyniki formuł są obecne po imporcie
    // ODS; style i żywe formuły nie. Odbuduj to, od czego zależy
    // potok niżej w łańcuchu, przed zapisem.
    Book.Sheets[0].Cells[2, 5].Formula := 'SUM(B2:D2)';
    Book.SaveAs('vendor-revision.xlsx');
  end;
finally
  Book.Free;
end;

Wzorzec architektoniczny, który z tego wypada: traktuj przychodzące pliki .ods jak strumienie danych, a nie jak dokumenty do edycji w miejscu. Trzymaj kanoniczny skoroszyt w .xlsx, wyczytuj wartości z poprawek klienta i emituj świeży ODS na żądanie z kopii kanonicznej. Weryfikacja należy do obu obozów - otwieraj wyeksportowane pliki w LibreOffice Calc, referencyjnym konsumencie ODF, i w Excelu, który czyta ODS od lat, ale rozchodzi się z LibreOffice na krawędziach wsparcia wykresów i stylów. Liczba arkuszy, garstka kluczowych komórek i obecność wykresów wystarczają za kontrolę dymną na profil eksportu

Selekcja pliku ODS przed zobowiązaniem się do importu

Kiedy punkt końcowy przyjmuje wysyłane pliki, wypisanie nazw arkuszy jest dużo tańsze niż pełne parsowanie i wcześnie łapie strukturalne niespodzianki:

Diagram bramki selekcji wysyłanych plików w HotXLS w Delphi, gdzie GetODSSheetNames odrzuca nieczytelne pakiety ODS i brakujące arkusze, zanim ruszy pełny import
Sonda GetODSSheetNames kosztuje dużo mniej niż pełne parsowanie i łapie awarię przemianowanego arkusza, póki błąd może jeszcze nazwać plik
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;

Konwencja zwracania podkłada ludziom nogę: wywołania HotXLS zwracają zwykle dodatnią liczność albo 1 przy powodzeniu i -1 przy porażce, czyszcząc przy niej listę, więc testuj <= 0, zamiast porównywać z jedną konkretną wartością dodatnią. GetODSSheetNames ani nie resetuje, ani nie wypełnia instancji skoroszytu, więc jeden obiekt sondujący może prześwietlić cały katalog przychodzących plików. Kontrole strukturalne tego rodzaju łapią najczęstszą awarię z prawdziwego świata - analityka przemianowującego albo usuwającego arkusz przed odesłaniem poprawki - już na bramce, gdzie komunikat błędu może jeszcze nazwać plik i brakujący arkusz, zamiast wychodzić jako referencja nil trzy warstwy głębiej

Jeśli budujesz wokół tego szerszy potok konwersji, wzorzec warsztatu audytu i konwersji skoroszytów pokazuje, jak zinwentaryzować funkcje pliku przed wyborem formatu docelowego, a przewodnik po wydajności dużych skoroszytów utrzymuje eksporty wsadowe w rozsądnych granicach pamięci

HotXLS to natywna biblioteka arkuszowa dla Delphi i C++Buildera z pełnym kodem źródłowym; kompletna lista funkcji i szczegóły licencjonowania są na stronie produktu HotXLS Delphi Component