Технічна стаття

Відкриття та збереження електронних таблиць ODS у Delphi з HotXLS

Бекенд звітності на Delphi, що роками видавав .xlsx, дістає нову вимогу: правила закупівель замовника з державного сектору наказують видавати OpenDocument Spreadsheet, а аналітики на цьому рахунку надсилають свої правки назад як файли .ods, збережені з LibreOffice. Тож тепер той самий код мусить і писати ODS, і читати його. HotXLS, нативна бібліотека електронних таблиць на Object Pascal від losLab для Delphi та C++Builder, працює в обох напрямках, і ніде не потрібно ні 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 — двійковий потік записів із 1990-х, який не має з ним нічого спільного

Це розміщення має практичний наслідок: спадкова книга .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 навмисно є легким шляхом. Він зберігає скалярні значення клітинок і кешований результат, який кожна формула несла на момент збереження, а також розгортає повторювані рядки й стовпці в сітку. Він не переносить стилів, виразів формул ODS чи малюнків

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

Режим збою, навколо якого треба проєктувати, випливає прямо звідси: таблиця, чиї підсумки були правильними, коли LibreOffice зберігав її востаннє, імпортується з правильними числами, але ці числа тепер константи. Відредагуйте вхідну клітинку, перерахуйте — і ніщо не зрушить: формули немає, лишився тільки її кінцевий результат. Якщо робочому процесу потрібні живі формули після імпорту, відновіть їх програмно зі своїх власних ділових правил через Cell.Formula, який на фасаді XLSX бере вираз без початкового знака рівності

Проєктування навколо несиметричного циклу читання-запису

Експорт промальовує з повної моделі книги в пам’яті: значення, стилі і, якщо попросите, діаграми та зображення. Імпорт повертає лише значення. Тож відрізок .xlsx у .ods має високу точність, а відрізок .ods у .xlsx повертає значення й кешовані результати, але ні оформлення, ні живих формул. Зчепіть ці два — і несиметричність помножиться. Повний цикл .xlsx у .ods у .xlsx записує все сумлінно по дорозі назовні й губить стилі та формули по дорозі назад, дарма що на жодному з кроків нічого не пішло не так

Діаграма асиметричного кола обміну ODS у HotXLS з Delphi: повноточний експорт з коміркової моделі книги в пам'яті та імпорт лише значень, що лишає формули константами
Export рендерить повну модель у пам'яті, тоді як import повертає значення та кешовані результати, тож повний цикл .xlsx в .ods і назад мовчки втрачає стилі та живі формули
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 до того, як братися за імпорт

Коли точка приймає завантаження, перелічити назви аркушів набагато дешевше, ніж повністю розібрати файл, і це рано ловить структурні несподіванки:

Діаграма шлюзу сортування завантажень 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