Technical Article

Отваряне и записване на ODS файлове в Delphi с HotXLS

Бекенд за отчети в Delphi, който от години генерира .xlsx, получава ново изискване: правилата за обществени поръчки на клиент от публичния сектор изискват изход под формата на OpenDocument Spreadsheet, а аналитиците по тази сметка изпращат редакциите си обратно как .ods файлове, записани от LibreOffice. Така че сега същият код трябва да записва ODS и да го чете. HotXLS, оригиналната Object Pascal библиотека за електронни таблици на losLab за Delphi и C++Builder, се справя и с двете посоки, без никъде да има инсталиран Excel или LibreOffice. Това, което тя не прави, е да направи двете посоки симетрични. Експортирането пренася много повече, отколкото импортирането възстановява, и екип, който предполага обратното, ще наблюдава как формулите и форматирането се изпаряват някъде между редакцията на клиента и следващия отчет, без да има конкретна грешка, която да го покаже.

Поддръжката на ODS живее във фасадата XLSX, а не в XLS

HotXLS доставя две независими йерархии от класове в един пакет: TXLSWorkbook в модул (unit) lxHandle за двоични BIFF8 .xls файлове и TXLSXWorkbook в модул lxHandleX за OOXML .xlsx пакети. Всяка входна точка за OpenDocument - OpenODS, SaveAsODS, GetODSSheetNames - е свързана с TXLSXWorkbook. Това разположение не е произволно. ODS пакетът, както е специфициран в OASIS ODF 1.3, е zip архив, съдържащ mimetype елемент, манифест и тяло content.xml, което го прави структурен братовчед на OOXML zip; BIFF8 е двоичен поток от записи от 90-те години на миналия век, с който нямат нищо общо.

Това разположение има практическа страна: остаряла .xls работна книга не може да се превърне в .ods с едно извикване. Първо прехвърляте съдържанието на BIFF в модела на XLSX чрез SaveXLSWorkbookAsXLSX от модул lxXlsxExport, отваряте отново резултата през TXLSXWorkbook и след това експортирате оттам. Този мост не е без загуба на данни и си струва да знаете пропуските, преди да изграждате решение върху него. Той копира стойности, формули, числови формати, шрифтове, запълвания и ширини на колони. Той изпуска граници, обединени диапазони, коментари, диаграми и условно форматиране. Източник в .xls с тежко форматиране ще достигне до ODS в по-опростен вид, отколкото е бил в началото, и това е свойство на моста, а не на ODS писателя.

Откриването от страна на импортирането е автоматично. Обикновеният метод Open разпознава ODS пакета по неговия mimetype елемент, като при липсата му преминава към проверка на най-горно ниво за content.xml, така че общ код от типа "отвори каквото потребителят е качил" не се нуждае от собствено разпознаване на разширения. След отварянето свойството SourceFormat отчита коя посока се е задействала.

Експортиране към ODS с TODSExportOptions

Самото извикване на експорт е на един ред; обектът с опции около него съдържа решенията, за които проверяващият ще попита по-късно:

var
  Book: TXLSXWorkbook;
  Opts: TODSExportOptions;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    Opts := TODSExportOptions.Create;        // caller owns and frees this
    try
      Opts.Generator := 'ReportService 4.2'; // meta:generator override
      Opts.IncludeCharts := True;
      Opts.IncludeImages := True;
      Book.SaveAsODS('quarterly-report.ods', Opts);
    finally
      Opts.Free;
    end;
  finally
    Book.Free;
  end;
end;

Обектът с опции е собственост на извикващия код. HotXLS няма да го освободи, поради което вътрешният блок try..finally е там и е задължителен. Двете свойства, които променят изходните данни, а не просто ги етикетират, заслужават по-подробен поглед. Задаването на IncludeCharts := False прави нещо повече от това да скрие диаграмите: то премахва поддокументите на диаграмите и техните записи в манифеста от пакета, което е точно това, което искате, когато консуматорът е конвейер за данни, който би се препънал в тях. Generator презаписва низа meta:generator на ODF, който иначе се чете като HotXLS/<version>; презапишете го, когато инструментите по веригата идентифицират производителите на файлове за насочване на поддръжката. Ако нищо от това не е приложимо, пропуснете изцяло обекта с опции. Извикването на SaveAs(FileName, xlsxOpenDocumentSpreadsheet) е същото като SaveAsODS с настройките по подразбиране, а претоварванията с потоци (streams) при двете ви позволяват да запишете пакета директно в HTTP отговор без временен файл.

Какво чете импортният път и какво умишлено пропуска

Прочетете тази част внимателно, преди да обещаете на някого двупосочна точност. Импортирането на ODS в HotXLS е умишлено олекотен процес. То запазва скаларните стойности на клетките и кеширания резултат, който всяка формула е носила по време на записването, и разширява повторените редове и колони в мрежата. То не пренася стилове, ODS изрази за формули или чертежи.

Изборът за формулите е този, който най-вероятно ще ви създаде проблеми, и той е направен умишлено. Клетка в ODF съхранява две неща едно до друго: израза на формулата, написан на диалекта OpenFormula, дефиниран в ODF 1.3 Част 4, и последната стойност, изчислена за нея от генериращото приложение. Преводът на OpenFormula в синтаксис на Excel формули е собствен проблем на преобразуване на диалекти, с реални гранични случаи около речниците на функциите, синтаксиса на препратките и моделите за грешки. Четенето на кешираната стойност вместо това избягва целия този клас от тихи грешки при превода, така че числата, които импортирате, са точно тези, които изпращачът е видял последно. Цената е, че те пристигат като числа, а не като активните формули, които са ги генерирали.

Режимът на отказ, който трябва да се предвиди при проектирането, произтича директно: електронна таблица, чиито общи суми са били правилни, когато LibreOffice я е записал последно, се импортира с правилни числа, но тези числа вече са константи. Редактирайте входяща клетка, преизчислете и нищо няма да се промени - формулата я няма, останал е само крайният й резултат. Ако работният процес се нуждае от активни формули след импортирането, възстановете ги програмно от вашите собствени бизнес правила чрез Cell.Formula, което във фасадата XLSX приема израза без водещ знак за равенство.

Проектиране около асиметричния двупосочен пренос

Експортирането рендира от пълния модел на работната книга в паметта: стойности, стилове и, ако ги поискате, диаграми и изображения. Импортирането връща само стойности. Така че етапът от .xlsx към .ods е с висока точност, а етапът от .ods към .xlsx връща стойности и кеширани резултати, но без стилизиране и без активни формули. Свържете двете и асиметрията се задълбочава. Пълен цикъл от .xlsx към .ods и обратно към .xlsx записва всичко вярно на излизане и губи стиловете и формулите на влизане, въпреки че нищо не се е провалило при нито една от стъпките.

Book := TXLSXWorkbook.Create;
try
  Book.Open('vendor-revision.ods');          // format auto-detected
  if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
  begin
    // Values and cached formula results are present after an ODS
    // import; styles and live formulas are not. Rebuild whatever
    // the downstream pipeline depends on before saving.
    Book.Sheets[0].Cells[2, 5].Formula := 'SUM(B2:D2)';
    Book.SaveAs('vendor-revision.xlsx');
  end;
finally
  Book.Free;
end;

Архитектурният модел, който произтича от това: третирайте входящите .ods файлове като канали за данни, а не като документи за редактиране на място. Поддържайте каноничната работна книга в .xlsx, четете стойностите от редакциите на клиентите и излъчвайте нов ODS при поискване от каноничното копие. Верификацията принадлежи и на двата лагера - отваряйте експортираните файлове в LibreOffice Calc, еталонния потребител на ODF, и в Excel, който чете ODS от години, но се разминава с LibreOffice по границите на поддръжката на диаграми и стилове. Броят на листовете, шепа ключови клетки и наличието на диаграми правят достатъчна бърза проверка (smoke check) за всеки експортен профил.

Триаж на ODS файл преди ангажиране с импортиране

Когато дадена крайна точка приема качвания, списването на имената на листовете е много по-евтино от пълен парс и улавя структурни изненади рано:

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;

Конвенцията за връщане на резултат обърква хората: извикванията на HotXLS обикновено връщат положителен брой или 1 при успех и -1 при неуспех, като изчистват списъка при неуспех, така че тествайте за <= 0, вместо да сравнявате с една конкретна положителна стойност. GetODSSheetNames нито нулира, нито попълва инстанцията на работната книга, така че единичен обект за сондиране може да провери цяла директория с входящи файлове. Структурни проверки като тази улавят най-често срещания провал в реалния свят - аналитик, който преименува или изтрива лист, преди да изпрати редакцията обратно - още на входа, където съобщението за грешка все още може да назове файла и липсващия лист, вместо да се появи като nil препратка три слоя по-дълбоко.

Ако изграждате по-широк конвейер за преобразуване около това, моделът на работен плот за одит и преобразуване на работни книги показва как да направите опис на характеристиките на файла, преди да изберете целевия формат, а ръководството за производителност при големи работни книги поддържа пакетните експорти в разумни граници на паметта.

HotXLS е оригинална библиотека за електронни таблици за Delphi и C++Builder с пълен изходен код; пълният списък с функции и подробности за лицензирането са на страницата на продукта HotXLS Component.