Серверная система отчётности на Delphi, годами выдававшая .xlsx, получает новое требование: правила закупок клиента из госсектора обязывают выводить в формате OpenDocument Spreadsheet, а аналитики этого клиента присылают свои правки обратно в виде файлов .ods, сохранённых из LibreOffice. Так что теперь тот же код должен и записывать ODS, и читать его. HotXLS, нативная библиотека Object Pascal для Delphi и C++Builder от losLab, справляется с обоими направлениями без установленных где-либо Excel или LibreOffice. Чего она не делает — так это не делает эти два направления симметричными. Экспорт несёт значительно больше, чем восстанавливает импорт, и команда, предполагающая обратное, будет наблюдать, как формулы и форматирование испаряются где-то между правкой клиента и следующим отчётом, без единой ошибки, на которую можно было бы указать
Поддержка 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 сообщает, какая ветка сработала
Экспорт в 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 намеренно является облегчённым путём. Он сохраняет скалярные значения ячеек и закэшированный результат, который каждая формула несла на момент сохранения, и разворачивает повторяющиеся строки и столбцы в полную сетку. Он не переносит стили, выражения формул ODS или рисунки
Решение по формулам — то, что скорее всего укусит, и оно было принято намеренно. Ячейка ODF хранит две вещи бок о бок: выражение формулы, записанное на диалекте OpenFormula, определённом в ODF 1.3 Part 4, и последнее значение, которое для неё вычислило создавшее её приложение. Перевод OpenFormula в синтаксис формул Excel — это своя отдельная задача конвертации диалекта, с реальными граничными случаями вокруг словарей функций, синтаксиса ссылок и моделей ошибок. Чтение закэшированного значения вместо этого обходит весь этот класс молчаливых ошибок перевода, так что импортируемые вами числа — это ровно те числа, которые последними видел отправитель. Цена в том, что они прибывают как числа, а не как живые формулы, которые их произвели
Отсюда прямо следует режим сбоя, вокруг которого нужно проектировать архитектуру: таблица, чьи итоги были верны в момент последнего сохранения в LibreOffice, импортируется с верными числами, но теперь эти числа — константы. Отредактируйте входную ячейку, пересчитайте — и ничего не сдвинется: формула исчезла, остался только её финальный результат. Если рабочему процессу после импорта нужны живые формулы, восстанавливайте их программно из собственных бизнес-правил через Cell.Formula, которое на фасаде XLSX принимает выражение без ведущего знака равенства
Проектирование вокруг асимметричного обратного цикла
Экспорт рендерит из полной модели книги в памяти: значения, стили и, если вы их запросили, диаграммы и изображения. Импорт возвращает только значения. Так что участок .xlsx в .ods обладает высокой точностью, а участок .ods в .xlsx приносит обратно значения и закэшированные результаты, но без оформления и без живых формул. Соедините эти два участка, и асимметрия усиливается. Полный цикл .xlsx в .ods и обратно в .xlsx честно записывает всё на выходе и теряет стили и формулы на обратном пути, хотя ни на одном из шагов ничего не пошло не так
Book := TXLSXWorkbook.Create;
try
Book.Open('vendor-revision.ods'); // формат определяется автоматически
if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
begin
// После импорта ODS присутствуют значения и закэшированные
// результаты формул; стилей и живых формул нет. Восстановите то,
// от чего зависит нижестоящий конвейер, перед сохранением
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 перед тем, как приступить к импорту
Когда конечная точка принимает загрузки, перечисление имён листов гораздо дешевле полного разбора и ловит структурные сюрпризы на раннем этапе:
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