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

Експорт діапазону клітинок Excel одним зображенням у HotXLS

Часом результат — не документ, а картина таблиці. Зведений блок у статусному листі, відрендерена панель KPI на панелі приладів, мініатюра поруч із результатом пошуку: усі вони хочуть клітинки і жоден не хоче папір. TXLSCellImageExporter у HotXLS бере класичний чи XLSX прямокутник клітинок і творить один компактний PNG чи JPEG без розміру сторінки, без полів, без колонтитулів, без наскрізних рядків друку та без розривів сторінок. Роздільність, масштаб, формат і якість JPEG налаштовуються, об'єкти, лінії сітки та межі клітинок мають окремі перемикачі, тло може бути кольором або прозорим, а запис файла йде через атомарну заміну в тій самій теці, що лишає наявну ціль недоторканою, якщо що-небудь зазнає невдачі

Причина, чому це потребує власного експортера, а не прапорця на шляху друку: розбивка на сторінки — не необов'язковий шар, який можна вимкнути. Це те, заради чого існує сторінковий конвеєр

Чому не рендерити діапазон крізь друкований конвеєр?

Бо друкований конвеєр вставляє сторінку між вами та клітинками. Розмір паперу вирішує, скільки вміщується, поля штовхають вміст усередину, колонтитули займають смуги, яких ви не просили, наскрізні рядки друку повторюють рядки, які у вас уже є, а розриви сторінок ріжуть діапазон. Зведений блок, що випадково сідає на розрив, виходить двома зображеннями з цікавим рядом, розрізаним навпіл. Можна все це компенсувати, налаштувавши власний розмір сторінки, що точно збігається з діапазоном, і люди так роблять, але це означає перераховувати геометрію паперу щоразу, коли діапазон змінюється, і досі лишає логіку смуги колонтитула та наскрізних рядків на шляху

Експортер клітинок міряє прямокутник, розподіляє бітову карту точно того розміру, малює клітинки в неї та кодує. Немає сторінки — немає чого вимикати. Для випадків, коли вам справді потрібен папір, правильний інструмент — шлях експорту PDF, охоплений у статті про експорт аркуша в PDF

TXLSCellImageExporter міряє, малює та кодує одне зображення на діапазон клітинок, тоді як друкований конвеєр ріже діапазон на розривах сторінок
Сторінковий конвеєр вставляє геометрію паперу між вами та клітинками; у експортера клітинок немає сторінки ніде на шляху

Міряйте перед рендерингом

Measure повертає піксельні розміри, які дадуть поточні налаштування, не кодуючи нічого. Це важить із двох причин. Шаблон HTML чи листа зазвичай потребує розмірів зображення до того, як зображення існує, щоб забронювати коробку та уникнути зсуву компонування. І служба, що рендерить обрані користувачем діапазони, потребує способу відхилити абсурдний запит до розподілу під нього

uses
  lxHandleX, lxPagination;

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Exporter: TXLSCellImageExporter;
  Summary: TXLSXRange;
  W, H, Bytes: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarter.xlsx');
    Sheet := Book.Sheets.ByPos[0];
    Summary := Sheet.Range['A1:F20'];

    Exporter := TXLSCellImageExporter.Create;
    try
      Exporter.ImageFormat := xpifPng;   // PNG тримає тонкі штрихи чіткими
      Exporter.DPI := 96;
      Exporter.Scale := 2.0;             // результат щільності retina
      Exporter.IncludeGridlines := False;
      Exporter.IncludeCellBorders := True;
      Exporter.TransparentBackground := True;
      Exporter.MaxPixels := 40 * 1000 * 1000;
      Exporter.MaxBytes := 8 * 1024 * 1024;

      if not Exporter.Measure(Summary, W, H) then
        raise Exception.Create('range exceeds the configured budget');
      // W і H тепер відомі; забронюйте коробку компонування до кодування
      Bytes := Exporter.Save(Summary, 'summary.png');
      if Bytes <= 0 then
        raise Exception.Create('image export failed, previous file kept');
    finally
      Exporter.Free;
    end;
  finally
    Book.Free;
  end;
end;

Бюджети, бо масштаб множиться

MaxPixels та MaxBytes — не оборонна декорація. Кількість пікселів росте з квадратом множника масштабу та з квадратом співвідношення роздільності, тож діапазон, розумний як 1200 на 800 за 96 DPI, стає приблизно 47 мегапікселів за 600 DPI, а користувач, що вибрав цілий ужитий діапазон замість зведеного блоку, додає ще один порядок величини зверху. Без стелі режим збою — розподіл, який процес не може задовольнити, що валить усе інше, чим той процес займався

Зі стелею запит зазнає невдачі, а викликач обирає: відмовити, знизити масштаб чи звузити діапазон. Це набагато краща позиція для звітного сервера, і це та сама логіка, що за явними бюджетами в декодері метафайлів, описаному в статті про обмежений декодер EMF і WMF

Потік бюджету TXLSCellImageExporter у HotXLS: Measure спершу повертає піксельний розмір, потім MaxPixels і MaxBytes обмежують розподіл та розмір результату
Відмова відбувається до розподілу, а невдача байтового бюджету лишає попереднє зображення недоторканим для викликача

Атомарна заміна і чому тека має значення

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

Деталь про ту саму теку не випадкова. Атомарна заміна атомарна лише в межах одного тому, бо між томами операційна система мусить копіювати, а тоді вилучати, що знову вводить вікно, яке ви намагалися закрити. Будь-яка реалізація цього взірця, що кладе тимчасовий файл у системну теку тимчасових файлів, не атомарна на машині, де результат живе на іншому диску

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

Події малювання пишуть на справжній канві

І експортер діапазону, і експортер сторінок відкривають початкові та кінцеві події малювання, і вони отримують повний контекст лише для читання, а не лише дескриптор канви. TXLSPagePaintContext несе живу канву, піксельні межі, розмір сторінки в пунктах, роздільність і масштаб, які справді вживаються, номер сторінки документа, номер сторінки всередині аркуша, загальну кількість сторінок, назву аркуша та вихідний робочий аркуш у класичному та XLSX варіантах. Цього досить, щоб намалювати водяний знак, що масштабується правильно, чи сторінковий штамп, що знає, де він у пробігу

procedure TReportJob.StampDraft(Sender: TObject;
  const AContext: TXLSPagePaintContext);
begin
  // З урахуванням масштабу, тож штамп виглядає однаково за 1x і 3x
  AContext.Canvas.Font.Height := Round(-48 * AContext.Scale);
  AContext.Canvas.Font.Color := clSilver;
  AContext.Canvas.Brush.Style := bsClear;
  AContext.Canvas.TextOut(AContext.Bounds.Left + Round(24 * AContext.Scale),
    AContext.Bounds.Top + Round(24 * AContext.Scale), 'DRAFT');
end;

Exporter.AfterPaint := Job.StampDraft;

Три поведінки варти покладання. Події спрацьовують рівно по разу на відрендерений кадр, включно з кожним кадром багатосторінкового TIFF, тож лічильник, збільшений в обробнику, довірливий. Вони мовчать під час вимірювання, тож обробник із побічним ефектом не виконається двічі на один результат. І якщо початкова подія викидає, кінцева не спрацьовує і жодних часткових байтів зображення не записано, тож виняток у вашому власному коді малювання не може дати напівштампований файл

Вибір формату

PNG для всього, багатого на текст. JPEG застосовує блочну трансформацію, що творить видиме дзвоніння навколо тонких висококонтрастних штрихів — а це точно межі клітинок і дрібний текст, і артефакти виживають за налаштуваннях якості, за яких фотографія виглядає бездоганно. JPEG виправдовує своє місце, коли діапазон домінований вбудованими фотографіями і розмір файла важить більше за вірність країв. Прозорі тла потребують PNG, бо JPEG не має альфа-каналу, тож плитка, покликана сидіти на кольоровій поверхні, уже зробила вибір за вас

Якщо ваш діапазон містить об'єднані клітинки, звіряйте результат з аркушем: об'єднані області взаємодіють із шириною стовпчиків так, що це дивує людей, і правила компонування охоплені в статті про об'єднані клітинки та шаблони звітів. HotXLS читає та пише XLS, XLSX, ODS і CSV з Delphi та C++Builder без залежності від Excel, а повна поверхня експортера задокументована на сторінці продукту HotXLS Delphi spreadsheet component