PDFlibPas умеет размечать документ прямо во время отрисовки. Включите SetAutoTagMode, и обычные вызовы DrawText становятся абзацами, текст, нарисованный сразу после RegisterHeading, становится заголовком соответствующего уровня, колонтитулы превращаются в артефакты, которые читалка пропускает, изображения становятся рисунками, а DrawTableRows переносит таблицу, её строки и ячейки в дерево структуры
Альтернатива — до недавнего времени единственный вариант — заключала каждый вызов рисования в пару BeginTag и EndTag вручную. Это работает, и для документов с необычной структурой остаётся правильным инструментом. Но для обычного отчёта, счёта или выписки это означает, что доступность вывода зависит от того, что никто нигде и никогда не забудет закрыть пару, причём на каждом пути кода, который хоть что-то рисует
Что покрывают биты режима
SetAutoTagMode принимает битовую маску и возвращает ранее действовавший режим. AUTOTAG_TEXT (1) размечает текст как абзац, либо как заголовок, если он ожидается. AUTOTAG_FURNITURE (2) помечает колонтитулы и номера страниц как артефакты. AUTOTAG_FIGURE (4) превращает нарисованное изображение в рисунок, либо в артефакт, если оно объявлено декоративным. AUTOTAG_TABLE (8) переносит нарисованные таблицы в дерево структуры. AUTOTAG_DEFAULT равно 15, то есть все четыре значения
Включение режима также помечает документ как размеченный, и этот шаг важнее, чем кажется. Читалка считает документ неразмеченным, пока каталог не утверждает обратное (ISO 32000-1 §14.7.1), поэтому файл с полным деревом структуры, но без объявления /MarkInfo объявляется вспомогательными технологиями как вообще не имеющий структуры. Дерево есть; никто его не читает
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetAutoTagMode(AUTOTAG_DEFAULT); // text + furniture + figures + tables
Lib.AddStandardFont(4);
Lib.SetTextSize(18);
Lib.RegisterHeading(1, 'Annual service report');
Lib.DrawText(72, 96, 'Annual service report'); // becomes H1
Lib.SetTextSize(11);
Lib.DrawText(72, 130, 'Every unit installed before 2024 was inspected.');
Lib.SaveToFile('report.pdf');
finally
Lib.Free;
end;
end;
Откуда заголовок знает, к какому тексту он относится?
RegisterHeading задаёт уровень для следующего нарисованного текста и ждёт именно текста. Если между ними рисуется изображение, оно становится рисунком, а заголовок остаётся в ожидании текста, который последует. Такое поведение намеренно: альтернатива, в которой изображение принимает уровень заголовка, порождала документы, где декоративная линейка под заголовком зачитывалась как сам заголовок
То же правило «расходуется на один элемент» управляет и рисунками. RegisterFigure задаёт описание, которое несёт следующее изображение, а RegisterDecoration объявляет следующее изображение линейкой, рамкой или фоном, не несущими смысла. Оба вызова поглощаются одним изображением, поэтому более позднее изображение никогда не наследует описание, предназначенное предыдущему — именно так альтернативный текст в ручной разметке оказывается прикреплён к неправильной картинке
Описание важнее любой другой строки в доступном документе. Незрячий читатель получает описание вместо изображения, и это всё, что он получает. «График» — это не описание; «Квартальная выручка по регионам, восточный регион лидирует в третьем квартале» — это описание
Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0); // becomes a tagged Figure
Lib.RegisterDecoration; // meaningless rule
Lib.AddImageFromFile('divider.png', 0); // drawn inside a layout artifact
Таблицы, заголовки и где живёт решение о повторении
С включённым битом таблиц DrawTableRows переносит таблицу, её строки и ячейки в дерево структуры, поэтому читалка может сообщить, в каком столбце находится значение, а не зачитывать всю таблицу как цепочку несвязанного текста. SetTableHeaderRowCount указывает, сколько первых строк являются заголовочными; эти строки записываются как ячейки заголовка с областью действия по столбцу, что и позволяет читалке объявить заголовок значения, на котором находится пользователь
Строки заголовка, заданные таким образом, остаются там, где они есть. Повторение их вверху каждой страницы — это решение о вёрстке, и оно им остаётся: DrawTaggedTableRows принимает аргумент RepeatHeaderRows именно для этой цели. Разведение этих двух аспектов позволяет избежать появления второй копии заголовка в дереве структуры при каждом разрыве страницы, что и породил бы автоматический повтор
var
TableID: Integer;
begin
TableID := Lib.CreateTable(40, 3);
Lib.SetTableHeaderRowCount(TableID, 1); // row 1 is the header band
Lib.SetTableCellContent(TableID, 1, 1, 'Part');
Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
// ... fill the data rows ...
// Draw rows 1..40 into a 600pt band, repeating one header row per page
Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;
Смешивание автоматической и ручной разметки
Автоматическая разметка отступает внутри тега, открытого вручную. Часть документа может быть описана вашим кодом, а остальное оставлено библиотеке, без взаимного вложения — именно такое расположение нужно большинству реальных документов. Титульная страница и блок подписи имеют структуру, которую понимаете только вы; двести страниц основного текста между ними — нет
Два защитных правила сохраняют вывод чистым. Внутри артефакта ничего не размечается, потому что содержимое, помеченное как артефакт, не должно нести элемент структуры. И пустой текст не открывает элемент, поэтому вызов DrawText с пустой строкой не может породить элемент структуры, который читалка зачитала бы как пустой. Оба дефекта — того типа, что документы с ручной разметкой накапливают незаметно, а валидатор спустя месяцы выдаёт списком
Что автоматическая разметка всё ещё не решает за вас
Порядок чтения за пределами порядка отрисовки, семантические роли, не являющиеся абзацем, заголовком, рисунком или таблицей, и объявления языка. Автоматическая разметка назначает структуру в порядке отрисовки содержимого: если ваш код вёрстки рисует боковую панель перед основным текстом, именно этот порядок фиксируется в дереве. Для документов, где визуальный порядок и порядок чтения действительно различаются, ручной API разметки остаётся правильным инструментом, а разбор tagged PDF и структуры доступности подробно описывает роли, области действия и привязки заголовков
Завершив документ, проверяйте, а не предполагайте: заметки о профилировании PDF/A и PDF/UA показывают, как получить вердикт по построенной структуре, а разбор экспорта отчётов на основе набора данных описывает, куда эти вызовы встают в движке отчётов, генерирующем вёрстку из данных
PDFlibPas — это нативная Pascal-библиотека PDF для Delphi, C++Builder и Lazarus без внешнего PDF-рантайма, поэтому доступный вывод создаётся тем же кодом, что и рисует документ — см. страницу продукта PDFlibPas для полного списка API и платформ