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

Диапазон ячеек 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 — обычный симптом наивной записи, которая открывает назначение и начинает поток

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

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