Иногда результат работы — не документ, а картинка таблицы. Блок сводки в отчёте о состоянии по почте, отрисованная панель KPI в дашборде, миниатюра рядом с результатом поиска: всем им нужны ячейки, и никому не нужна бумага. TXLSCellImageExporter в HotXLS берёт прямоугольник ячеек классического или XLSX формата и выдаёт один компактный PNG или JPEG без размера страницы, без полей, без колонтитулов, без сквозных строк и без разрывов страниц. Разрешение, масштаб, формат и качество JPEG настраиваются, объекты, линии сетки и границы ячеек имеют независимые переключатели, фон может быть цветным или прозрачным, а запись файла идёт через атомарную замену в той же папке, которая не трогает существующий целевой файл, если что-то идёт не так
Причина, по которой этому нужен собственный экспортёр, а не флаг на пути печати, в том, что разбиение на страницы — не опциональный слой, который можно выключить. Это то, ради чего конвейер страниц существует
Почему не отрисовывать диапазон через конвейер печати?
Потому что конвейер печати вставляет страницу между вами и ячейками. Размер бумаги решает, сколько поместится, поля сдвигают содержимое внутрь, колонтитулы занимают полосы, которых вы не просили, сквозные строки повторяют строки, которые у вас уже есть, а разрывы страниц режут диапазон. Блок сводки, случайно попавший на разрыв, выходит двумя изображениями с интересующей строкой, разрезанной пополам. Всё это можно компенсировать, настроив пользовательский размер страницы, точно совпадающий с диапазоном, и так делают, но это означает пересчёт геометрии бумаги при каждом изменении диапазона, а логика полосы колонтитула и сквозных строк всё равно остаётся в пути
Экспортёр ячеек измеряет прямоугольник, выделяет битовую карту точно этого размера, рисует ячейки в неё и кодирует. Страницы нет, поэтому настраивать нечего. Для случаев, когда бумага всё же нужна, правильный инструмент — путь экспорта PDF, описанный в статье об экспорте листа в PDF
Измеряйте, прежде чем отрисовывать
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
Атомарная замена и почему важна папка
Save в имя файла не пишет прямо в цель. Он пишет временный файл в той же папке, кодирует в него и только затем заменяет цель. Если кодирование падает, если лимит превышен на полпути или если процесс убит, предыдущее изображение всё ещё там и всё ещё валидно. Дашборд, перегенерирующий плитки по расписанию, поэтому никогда не показывает обрезанный PNG — обычный симптом наивной записи, которая открывает назначение и начинает поток
Деталь про ту же папку не случайна. Атомарная замена атомарна только внутри одного тома, потому что между томами операционной системе приходится копировать, а затем удалять, что заново открывает окно, которое вы пытались закрыть. Любая реализация этого шаблона, кладущая временный файл в системный каталог temp, не атомарна на машине, где вывод живёт на другом диске
События рисования рисуют на настоящем холсте
И экспортёр диапазона, и экспортёр страниц предоставляют события рисования до и после, и они получают полный контекст только для чтения, а не просто дескриптор холста. 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