HotPDF рендерит HTML-таблицы через свой HTML5-профиль печатной вёрстки, используя настоящую сетку занятости для rowspan и colspan, замеренные высоты строк вместо оценок по числу символов и повторяющуюся на каждой странице-продолжении шапку. Две ситуации заставляют его отказаться повторять шапку, и знать их заранее дешевле, чем потом отлаживать задублированную ячейку
Класс документов, который это требует, знаком каждой команде отчётности: счёт или комплаенс-отчёт, где источник истины — HTML, таблица тянется на четыре страницы, и шапка обязана читаться на каждой. Всё, что меньше настоящей табличной вёрстки, даёт два отказа, которые читатели замечают сразу: шапку, появившуюся один раз на первой странице, и высоты строк, угаданные по числу символов
Почему табличные возможности переехали в HTML-рендерер?
Потому что альтернатива теряет rich text, а rich text — та причина, по которой контент вообще HTML. Очевидный план выглядит как переиспользование: у HotPDF уже есть layout DOM-объект таблицы с настоящей сеткой, так что просто перекинь HTML-парсер в него и получи объединение ячеек бесплатно. Проблема в том, чем этот табличный объект рисует. Его ячейки несут текст и стиль, а путь рисования выдаёт plain text, так что всё, что HTML реально содержал помимо шрифта и цвета, — ссылки, верхние индексы, инлайновые смены размера, цвет по диапазону, — к моменту попадания на страницу уже потеряно
Направление, которое выживает при встрече с реальными документами, — обратное. Перенесите возможности табличного движка — сетку занятости, настоящий замер, повтор шапки и взвешивание колонок — в HTML-рендерер и оставьте rich-text-рендеринг там, где он уже работает. Это большее изменение, чем мост, и именно оно сохраняет гиперссылку внутри ячейки таблицы гиперссылкой
Rowspan без union-find
Объединённые ячейки создают атомарные группы строк, но замыкание по этим группам не нуждается в общей структуре disjoint set, потому что занятость всегда непрерывный интервал. Ячейка с rowspan="3", начинающаяся в строке K, занимает строки с K по K+2 и ничего больше, поэтому информация о группах сводится к маркеру конца на строку
Алгоритм — две строки намерения. Размещая объединённую ячейку, которая начинается в K и заканчивается в E, запишите GroupEnd[K] := Max(GroupEnd[K], E). Затем один раз пройдите строки в обратном порядке, применяя G[R] := G[G[R]], — это протаскивает конец каждой строки назад сквозь перекрывающиеся объединения и даёт транзитивное замыкание за один проход. На выходе для каждой строки — последняя строка, которая обязана остаться с ней на одной странице, а это ровно то, что шагу пагинации нужно, чтобы решить, где может упасть разрыв
Раздача высоты — вторая половина. Когда объединённой ячейке нужно больше вертикального пространства, чем сейчас дают покрытые ею строки, излишек идёт в последнюю строку объединения, а не размазывается по всем. Обрабатывайте объединённые ячейки после того, как устоялись обычные высоты строк, а затем добирайте последнюю строку каждого объединения. Равномерное размазывание кажется справедливее и даёт заметно неправильный вывод: строки, содержащие только короткие однострочные ячейки, раздуваются потому, что какая-то посторонняя ячейка тремя строками выше случайно оказалась высокой
var
Pdf: THotPDF;
Importer: THPDFHTMLImporter;
Stats: THPDFHTMLImportStatistics;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'audit-report.pdf';
Pdf.BeginDoc;
Importer := THPDFHTMLImporter.Create(Pdf);
try
Importer.Margin := 48;
Importer.BaseFontName := 'Arial';
Importer.BaseFontSize := 10;
Importer.MaxDOMNodes := 200000;
Importer.MaxLayoutOperations := 2000000;
if Importer.RenderHTML5(SourceHtml, PrintStyleSheet) then
begin
Stats := Importer.Statistics;
Writeln('tables ', Stats.TableCount,
' page breaks ', Stats.PageBreakCount);
end;
finally
Importer.Free;
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
RenderHTML5 принимает необязательный авторский стиль вторым аргументом, и именно туда относятся print-правила. Экранный стиль туда не тащите. Профиль версионирован, и HTML5ProfileMilestones сообщает, какие группы возможностей реализует текущая сборка: himParserCascade, himPagedLayout, himTablesForms и himBoundedResources, — так что приложение может деградировать осознанно, а не обнаруживать дыру в продакшене
Замер обязан сходиться с рисованием, точно
Высота строки корректна только тогда, когда код, замеряющий перенесённые строки, переносит их по тому же правилу, что и код, который их рисует. Звучит очевидно и при этом — самый частый источник таблиц, чьи рамки не сходятся с содержимым. HotPDF замеряет жадным счётчиком строк, и этот счётчик обязан совпадать с семантикой переноса rich-text-пути вывода в трёх конкретных отношениях: разрыв только по пробелам, слово никогда не режется, а слово шире колонки получает собственную строку
Второе требование — шрифт. Замер должен идти со шрифтом самой ячейки, выставленным через SetFont с реальным именем, набором стилей и размером до вызова функции ширины, а не с тем шрифтом, который случайно был активен. Жирный текст при том же размере стабильно шире обычного больше чем на десять процентов — этого хватает, чтобы трёхстрочная ячейка стала четырёхстрочной. Таблица, где ячейки шапки жирные, а ячейки тела нет, замеренная одним шрифтом, будет неправильной ровно в тех строках, куда читатели смотрят первыми
Сделав это правильно, вы меняете то, что можете проверять в тесте. Наблюдаемый эффект точного замера — межстрочное расстояние, а не количество глифов: однострочная строка высотой около 20 поинтов, тогда как оценка по числу символов того же контента предсказывает две строки и примерно 35. Проверяйте вертикальную дистанцию между строками. И помните, что в user space PDF ось Y растёт вверх, поэтому шапка над строкой тела означает, что Y шапки — большее значение, что противоположно экранно-координатной интуиции
Когда HotPDF отказывается повторять шапку?
В двух случаях, и оба без остановки дали бы заметно неправильный вывод. Первый — блок шапки, содержащий объединённую ячейку, которая заходит из шапки в строки тела. Повтор шапки нарисовал бы содержимое этой ячейки второй раз в позиции, где оно уже не принадлежит, поэтому шапка рисуется один раз, и таблица продолжается без неё. Второй — шапка выше 90 процентов полезной высоты страницы: повтор оставил бы почти нет места для данных, и таблица перестала бы продвигаться вперёд
Оба отказа намеренные и тихие по дизайну, потому что альтернатива хуже. Если шапка не повторилась, а вы ждали обратного, проверьте разметку на rowspan, пересекающий границу thead, прежде чем подозревать движок. Один этот паттерн разметки объясняет большинство сюрпризов
// Веса колонок берутся из разметки, поэтому print style sheet — то место,
// где ими управляют. Ширины трактуются как веса, а не как пиксели
const
PrintStyleSheet =
'table { width: 100%; }' +
'thead th { font-weight: bold; background: #eee; }' +
'td.amount { text-align: right; }';
// Строка шапки с rowspan, заходящим в тело, подавляет повтор шапки.
// Держите объединения внутри одной секции:
// <thead><tr><th rowspan="2">Item</th>...</tr></thead> ок
// <tr><th rowspan="3">Item</th>... заходит в tbody, повтора нет
Ширины колонок ведут себя как веса, а не абсолютные величины, — это то поведение, которое сохраняет таблицу пригодной, когда контент не совпал с авторской оценкой. Колонка, объявленная на 30 процентов, получает примерно 30 процентов доступной ширины, но раздача уважает минимальную ширину, которая каждой колонке реально нужна, поэтому узкая колонка с длинным неразрывным токеном не переполняет молча коробку таблицы
Где это место в документном пайплайне
Табличная работа сидит внутри более широкого профиля печатной вёрстки, и правила пагинации, бюджеты ресурсов и обработка CSS, описанные в пути импорта HTML5 paged media, применяются к документам с таблицами без изменений. Если ваши данные не начинаются как HTML, путь прямого конструирования из построения таблиц сразу в PDF минует слой парсинга и даёт то же поведение сетки через API. А поскольку высота строки в конечном счёте зависит от того, где рвутся строки, обсуждение замера в выключке текста и разрыве строк — компаньон для всех, кто тюнит плотный табличный вывод
Переиспользуемый урок здесь вообще не про таблицы. Когда новому подсистемному блоку нужна возможность, которая у старого уже есть, спросите, кто из двух владеет тем, что труднее всего заново реализовать. Арифметика сетки — пара десятков строк и переезжает легко. Rich-text-рендеринг с инлайновыми ссылками, верхними индексами и стилями по диапазонам — нет, поэтому сетка переехала, а текст остался. HotPDF поставляет оба пути в составе Delphi PDF-компонента HotPDF, так что выбор между HTML-входом и прямым конструированием — проектное решение, а не библиотечное