Техническая статья

HotXLS Delphi Component: ODS open, save, and round-trip в Delphi

Серверная система отчётности на Delphi, годами выдававшая .xlsx, получает новое требование: правила закупок клиента из госсектора обязывают выводить в формате OpenDocument Spreadsheet, а аналитики этого клиента присылают свои правки обратно в виде файлов .ods, сохранённых из LibreOffice. Так что теперь тот же код должен и записывать ODS, и читать его. HotXLS, нативная библиотека Object Pascal для Delphi и C++Builder от losLab, справляется с обоими направлениями без установленных где-либо Excel или LibreOffice. Эти два направления близки, но не идентичны. Начиная с HotXLS 2.88.229 путь импорта возвращает формулы как живые формулы рядом с их закэшированными результатами, а актуальные релизы читают также стили, числовые форматы, объединённые диапазоны, комментарии, изображения, диаграммы и условные форматы. По-прежнему не переживает круговой путь полная семантика OpenFormula и горстка деталей на уровне пакета, так что команда, ожидающая идеального зеркала, всё же столкнётся с граничными случаями где-то между правкой клиента и следующим отчётом, обычно без ошибки, на которую можно указать

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

HotXLS поставляет две независимые иерархии классов в одном пакете: TXLSWorkbook в модуле lxHandle для бинарных файлов BIFF8 .xls и TXLSXWorkbook в модуле lxHandleX для пакетов OOXML .xlsx. Каждая точка входа OpenDocument — OpenODS, SaveAsODS, GetODSSheetNames — висит на TXLSXWorkbook. Это размещение не случайно. Пакет ODS, как определено в OASIS ODF 1.3, — это zip-архив, несущий элемент mimetype, манифест и тело content.xml, что делает его структурным родственником zip OOXML; BIFF8 — это бинарный поток записей девяностых годов, не имеющий с этим ничего общего

У этого размещения есть практическая грань: устаревшая книга .xls не может стать .ods за один вызов. Сначала вы перебрасываете содержимое BIFF в модель XLSX через SaveXLSWorkbookAsXLSX из модуля lxXlsxExport, заново открываете результат через TXLSXWorkbook, а затем уже экспортируете оттуда. Этот мост не сохраняет точность полностью, и его пробелы стоит знать до того, как вы на него полагаетесь. Он копирует значения, формулы, числовые форматы, шрифты, заливки, границы, объединённые диапазоны, ширины столбцов и высоты строк. Он отбрасывает комментарии, диаграммы и условное форматирование. Источник .xls с насыщенным форматированием придёт в ODS выглядящим проще, чем ушёл, и это свойство моста, а не писателя ODS

На стороне импорта распознавание автоматическое. Обычный метод Open опознаёт пакет ODS по его элементу mimetype, с откатом к проверке верхнеуровневого content.xml, когда этот элемент отсутствует, так что общему коду пути «открыть то, что загрузил пользователь» не нужна собственная проверка расширения. После открытия свойство SourceFormat сообщает, какая ветка сработала

Диаграмма компоновки классов HotXLS в Delphi: каждая точка входа ODS живёт на TXLSXWorkbook, а мост SaveXLSWorkbookAsXLSX переносит содержимое BIFF8 .xls через
Каждая точка входа OpenDocument свисает с TXLSXWorkbook, а устаревший .xls добирается до ODS только через мост BIFF в XLSX с потерями

Экспорт в ODS с TODSExportOptions

Сам вызов экспорта — одна строка; объект параметров вокруг него несёт решения, о которых позже спросит рецензент:

var
  Book: TXLSXWorkbook;
  Opts: TODSExportOptions;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    Opts := TODSExportOptions.Create;        // вызывающий код владеет и освобождает это
    try
      Opts.Generator := 'ReportService 4.2'; // переопределение meta:generator
      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 переопределяет строку ODF meta:generator, которая иначе гласит HotXLS/<version>; переопределяйте её, когда нижестоящие инструменты снимают отпечаток производителя файла для маршрутизации в поддержку. Если ничего из этого не применимо, полностью пропустите объект параметров. Вызов SaveAs(FileName, xlsxOpenDocumentSpreadsheet) — то же самое, что SaveAsODS со значениями по умолчанию, а перегрузки с потоком у обоих позволяют записать пакет прямо в HTTP-ответ без временного файла

Что читает путь импорта — и что он по-прежнему пропускает

Прочитайте эту часть внимательно, прежде чем обещать кому-либо точность кругового пути. Импорт ODS в HotXLS начинался как облегчённый путь, хранивший только скалярные значения и закэшированные результаты формул, и с тех пор он ушёл далеко вперёд. Актуальные релизы читают content.xml вместе с styles.xml и settings.xml: скалярные значения и закэшированные результаты формул, повторяющиеся строки и столбцы, развёрнутые в полную сетку, объединённые диапазоны, форматированный текст, гиперссылки, комментарии, встроенные изображения и диаграммы, наследуемые и стандартные стили ячеек, числовые форматы и форматы дат, высоты строк и ширины столбцов, именованные диапазоны, закрепление областей, диапазоны AutoFilter, проверки данных и условные форматы. Чего OpenODS по-прежнему не конвертирует, так это полной семантики OpenFormula, ключей защиты ODS, геометрии комментариев, расширенного форматирования комментариев и функций пакета за пределами содержимого таблиц

Формулы — та часть, которая скорее всего укусит. Ячейка ODF хранит две вещи бок о бок: выражение формулы, записанное на диалекте OpenFormula, определённом в ODF 1.3 Part 4, и последнее значение, которое для неё вычислило создавшее её приложение. Ранние релизы читали только закэшированное значение. Начиная с HotXLS 2.88.229, OpenODS импортирует также выражение: ячейка, записанная с префиксом of:= или oooc:=, сохраняет своё закэшированное значение и получает нормализованную формулу Excel, причём ссылки в квадратных скобках вроде [.A1:.B1] и разделители аргументов с точкой с запятой конвертируются в синтаксис Excel. С версии 2.384.32 импорт конвертирует также встроенные строки-массивы ({1;2|3;4} превращается в {1,2;3,4}) и оператор объединения ссылок ~, а с версии 2.384.55 такое объединение читается обратно без второй пары скобок. Перевод OpenFormula в синтаксис Excel всё ещё остаётся задачей конвертации диалекта с реальными граничными случаями вокруг словарей функций, синтаксиса ссылок и моделей ошибок — именно поэтому рядом идёт закэшированное значение: импортируемые вами числа — это ровно те числа, которые последними видел отправитель

Режим сбоя, вокруг которого нужно проектировать, соответственно сузился. До 2.88.229 таблица, чьи итоги были верны в момент последнего сохранения в LibreOffice, импортировалась с верными числами, которые теперь были константами: отредактируйте входную ячейку, пересчитайте — и ничего не сдвинется. В актуальных релизах формулы возвращаются живыми, так что оставшийся риск — это конструкция, которую транслятор не моделирует: она может оставить формулу, пересчитывающуюся иначе, чем в создавшем её приложении, тогда как закэшированное значение всё ещё показывает результат отправителя. Сравнивайте пересчитанные результаты с импортированными закэшированными значениями для формул, от которых зависит ваш конвейер, и восстанавливайте любую разошедшуюся формулу из собственных бизнес-правил через Cell.Formula, которое на фасаде XLSX принимает выражение без ведущего знака равенства

Проектирование вокруг оставшихся пробелов кругового пути

Экспорт рендерит из полной модели книги в памяти: значения, формулы, стили и, если вы их запросили, диаграммы и изображения. Экспорт формул оттачивался релиз за релизом. С версии 2.384.55 встроенные массивы, объединения ссылок и абсолютные ссылки сохраняют свой смысл в OpenFormula; с версии 2.384.56 HotXLS объявляет пространства имён своих префиксов формул, так что LibreOffice вычисляет формулы вместо того, чтобы показывать #VALUE! в каждой ячейке с формулой; а с версии 2.384.65 ссылки на целые столбцы и строки записываются в форме OpenFormula [.A:.A], так что Excel больше не открывает их как #NAME?. Условные форматы подтянулись следом: с версии 2.384.66 правила на основе формул записываются как formula-is(...) с их базовой ячейкой, что LibreOffice распознаёт, а с версии 2.384.69 правила по значению и по формуле записываются также как стандартные условия style:map — единственная форма, которую читает Excel, — при этом сторона импорта читает обе формы. Полный цикл .xlsx в .ods и обратно в .xlsx на актуальном релизе поэтому сохраняет значения, формулы, стили и условные форматы; то, что он всё ещё может потерять, сидит в пробелах, перечисленных в предыдущем разделе

Диаграмма кругового пути ODS в HotXLS из Delphi: экспорт из модели книги в памяти и импорт, который ранние релизы ограничивали значениями, оставлявший формулы константами до HotXLS 2.88.229
Ранние релизы импортировали только значения и закэшированные результаты, поэтому полный цикл .xlsx в .ods в .xlsx терял стили и живые формулы; актуальные релизы читают обратно и то и другое
Book := TXLSXWorkbook.Create;
try
  Book.Open('vendor-revision.ods');          // формат определяется автоматически
  if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
  begin
    // Начиная с HotXLS 2.88.229 формулы возвращаются живыми рядом
    // с закэшированными результатами; перед сохранением восстановите
    // любую формулу, в перевод которой конвейер не может доверять
    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 на границах поддержки диаграмм и стилей. Число листов, горстка ключевых ячеек и наличие диаграмм — достаточная дымовая проверка на профиль экспорта

Сортировка файла ODS перед тем, как приступить к импорту

Когда конечная точка принимает загрузки, перечисление имён листов гораздо дешевле полного разбора и ловит структурные сюрпризы на раннем этапе:

Диаграмма шлюза сортировки загрузок HotXLS в Delphi: GetODSSheetNames отвергает нечитаемые пакеты ODS и отсутствующие листы до полного импорта
Зонд GetODSSheetNames стоит куда дешевле полного разбора и ловит сбой переименованного листа, пока ошибка ещё может назвать файл
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 Delphi Component