Доступный PDF опирается на одну структуру, которую видимая страница никогда не показывает: дерево структуры, определённое в ISO 32000-1 §14.7. Это логическая иерархия заголовков, абзацев, таблиц и рисунков, наложенная поверх отрисованного содержимого и сопоставленная со стандартными ролями через карту ролей. Программа чтения с экрана читает именно это дерево, а не отметки на странице. Без него сгенерированный счёт, который выглядит безупречно, семантически пуст, потому что поток содержимого фиксирует лишь порядок рисования и ничего больше. Итог может быть объявлен раньше строк заказа, подвал может врезаться в середину абзаца, таблица позиций может схлопнуться в один неразличимый поток слов. Цена предотвращения этого несоразмерно мала в вашу пользу. Формирование структуры по ходу рисования занимает считаные минуты кода; добавление её задним числом в готовые документы — это уже проект по устранению недостатков. losLab PDF Library (PDF Library for Delphi) открывает это дерево для Delphi и C++Builder через небольшой набор вызовов, каждый из которых оборачивает операцию рисования в её логическую роль
Как размеченное содержимое привязывается к дереву структуры
Здесь взаимодействуют два слоя. В потоке содержимого операции рисования заключены в последовательности размеченного содержимого, каждая из которых несёт целочисленный MCID. В каталоге документа дерево структуры сопоставляет эти MCID с иерархией типизированных элементов (H1, P, Table, Figure) с такими атрибутами, как альтернативный текст и язык. Пользовательские типы элементов допустимы, но каждый из них должен разрешаться в стандартную роль через карту ролей (ISO 32000-1 §14.8.4). Содержимое, вовсе не несущее смысла, например линии, фоны и повторяющееся оформление страницы, помечается как артефакт, чтобы вспомогательные технологии пропускали его, а не зачитывали посреди предложения
PDF Library for Delphi поддерживает оба слоя за одной парой скобок. BeginTag открывает элемент структуры и начинает последовательность размеченного содержимого, вызовы рисования попадают внутрь неё, а EndTag закрывает оба слоя сразу. Учёт, на котором спотыкается ручная разметка, — MCID, родительское дерево и ссылки на страницы, — происходит внутри библиотеки, там, где ошибиться невозможно
Два переключателя уровня документа задают рамки работы прежде, чем откроется хоть один тег. SetMarkInfo записывает в каталог флаг, объявляющий документ размеченным, а IsTaggedPDF считывает его обратно, что служит дешёвой первой проверкой при решении, стоит ли сохранять структуру входящего файла. У языка есть две точки входа. SetDocumentLanguage задаёт язык документа по умолчанию сам по себе, тогда как SetPDFUAMode задаёт его как часть включения полного вывода PDF/UA. Файл можно осмысленно разметить, не заявляя о соответствии PDF/UA, и поэтапное внедрение часто начинается именно с этого
Разметка во время рисования, а не после
Работающий шаблон формирования состоит в том, чтобы относиться к скобке тега как к части сигнатуры каждого вызова рисования, а не как к отдельному последующему проходу:
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1); // начало координат в левом верхнем углу
Lib.SetPDFUAMode('en-US'); // поднимает версию сохранения до PDF 1.7
Lib.SetInformation(1, 'Service Manual'); // /Title обязателен для PDF/UA
Lib.AddRoleMap('ManualTitle', 'H1'); // пользовательский тип -> стандартная роль
Lib.AddStandardFont(4);
Lib.SetTextSize(18);
Lib.BeginTagEx2('ManualTitle', '', '', 'en-US', '', 'h1-cover', '');
Lib.DrawText(72, 96, 'Service Manual');
Lib.EndTag;
Lib.BeginTag('Figure', 'Exploded view of the gearbox assembly', '');
Lib.AddImageFromFile('gearbox.png', 0);
Lib.EndTag;
Lib.BeginArtifact('Layout'); // оформление страницы: исключено из чтения
// ... нарисовать линии и фоновую заливку ...
Lib.EndArtifact;
Lib.SaveToFile('manual.pdf');
finally
Lib.Free;
end;
end;
Три вызова в этой последовательности несут вес соответствия. SetPDFUAMode включает вывод PDF/UA и незаметно поднимает версию документа до PDF 1.7, что вступает в конфликт с фиксацией версии. Документ, закреплённый на PDF 1.4 через LockSaveVersion, отказывается сохраняться и возвращает код ошибки 602, как только включён режим UA, — столкновение, которое часто всплывает, когда архивные профили и требования доступности настраивают разные команды. SetInformation(1, ...) записывает заголовок документа, который ISO 14289 ожидает увидеть в программе просмотра вместо имени файла; его отсутствие — одна из самых частых находок аудита PDF/UA на практике. AddRoleMap регистрирует пользовательский тип ManualTitle как H1, и пропуск этого шага оставляет описанную ниже диагностику отмечающей несопоставленную роль
Уровни заголовков заслуживают продуманной политики, а не решений, принятых на ходу ради вида страницы. Пользователи программ чтения с экрана переходят между разделами по горячей клавише заголовков, поэтому шаблон, который перескакивает с H1 на H3 потому, что промежуточный уровень выглядел слишком крупным в визуальном дизайне, незаметно ломает эту навигацию, и никакая визуальная проверка этого никогда не поймает. Это именно тот дефект, для обозначения которого существует диагностика HEADING-LEVEL-SKIP. Сопоставьте визуальные стили каждого шаблона с фиксированной лестницей заголовков один раз, в одном месте, и это расхождение никогда не начнётся
Таблицы, по которым программа чтения с экрана действительно может перемещаться
Нарисованные линии сетки вне экрана ничего не значат. Программы чтения с экрана перемещаются по структурным связям: какие ячейки являются заголовками, чем управляет каждый заголовок и как ячейки данных привязаны к заголовкам в нерегулярных раскладках. Все три задачи решают вызовы атрибутов элемента структуры:
Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column'); // действителен только пока этот TH открыт
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2); // заголовок охватывает колонки значения и единицы измерения
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part'); // явная привязка для нерегулярных таблиц
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table
Правило порядка строгое и соблюдается молча. Каждый вызов SetStructElem* применяется к тегу, открытому именно в этот момент, между его BeginTag и его EndTag, и он возвращает 0, не выбрасывая ничего, если ни один тег не открыт или атрибут не применим к текущему. Вызов не по месту просто исчезает бесследно. Оборачивание возвращаемых значений в проверки во время разработки ловит расхождение, пока вы ещё можете его увидеть; оставленное без внимания, отсутствие области видимости проявится лишь тогда, когда аудит доступности прогонит настоящую программу чтения с экрана по таблице. Идентификаторы элементов, переданные через BeginTagEx2, питают дерево идентификаторов (ISO 32000-1 §14.7.4), и именно это делает привязку SetStructElemHeaders вообще разрешимой
То же семейство атрибутов покрывает остальное, на что опираются вспомогательные технологии. SetStructElemListNumbering объявляет, как маркируются элементы списка, так что программа чтения с экрана озвучивает позицию в списке вместо перечисления значков маркеров. SetStructElemBBox записывает ограничивающий прямоугольник рисунков и таблиц, который используют режимы перекомпоновки для размещения содержимого. SetStructElemActualText предоставляет замещающий текст для участков, чьи глифы не сопоставляются с читаемыми символами, например для буквицы, собранной из векторной графики. Каждый из этих вызовов подчиняется одному и тому же правилу: он привязывается к открытому тегу или исчезает
Артефакты, язык и шлюз диагностики перед сохранением
Повторяющееся оформление страницы, то есть колонтитулы, метки сгиба, водяные знаки и фоновые заливки, должно находиться внутри скобок BeginArtifact и EndArtifact, чтобы никогда не попадать в поток чтения. Язык наследуется. Язык документа по умолчанию берётся из аргумента SetPDFUAMode, а фрагмент на другом языке переопределяет его для отдельного элемента через BeginTagEx или SetStructElemLang. Именно это делает французскую цитату внутри английского руководства произносимой
Перед сохранением GetPDFUADiagnostics прогоняет структурные проверки библиотеки по документу в памяти и возвращает результаты в виде текста, где пустая строка означает, что ничего не найдено. Коды прямо называют классические ошибки авторства: FIGURE-NO-ALT для изображения без альтернативного текста, HEADING-LEVEL-SKIP для H3, следующего сразу за H1, ROLEMAP-UNMAPPED для пользовательского типа, который так и не был зарегистрирован. Подключите это к сборке (сгенерируйте набор документов, провалите шаг при непустой диагностике), и регрессии доступности станут ошибками уровня компиляции вместо находок аудита месяцы спустя. Итоговый вердикт о соответствии по-прежнему остаётся за преflight-проверкой сохранённого файла, который рассмотрен в статье Preflight-проверка PDF/A и PDF/UA в Delphi, потому что некоторые нормализации применяются только во время сериализации
У навигации по аннотациям есть собственный переключатель. PDF/UA ожидает, что перемещение по полям форм и ссылкам с клавиатуры будет следовать порядку структуры, и SetTabOrderMode записывает запись порядка табуляции на уровне страницы, которую соблюдают программы просмотра, а GetTabOrderMode доступен для проверки входящих файлов. Это требование, которое никто не замечает, пока пользователь, работающий только с клавиатуры, не заявит об ошибке, а исправляется оно одним вызовом на документ
Деревья структуры переживают не каждое слияние
Размеченные документы остаются размеченными только тогда, когда каждый последующий шаг обработки сохраняет дерево, и острый край внутри PDF Library for Delphi — это семейство функций слияния списков файлов. MergeFileListFast обменивает сохранение дерева структуры на скорость. Это правильный компромисс для пакетов сканированных изображений и неправильный для размеченных отчётов, потому что итоговый файл открывается нормально, отрисовывается идентично и незаметно теряет слой доступности. Используйте MergeFileList по умолчанию или строгий вариант всякий раз, когда любой из входных файлов размечен, и сделайте IsTaggedPDF частью проверок после сборки, чтобы сведённый в плоскую структуру пакет не мог уйти в печать незамеченным. Конвейеры сборки для больших наборов документов несут больше компромиссов такого рода, рассмотренных в статье Слияние, разделение и прямой доступ для больших PDF
Цикл проверки замыкается вне библиотеки: откройте результат в Acrobat, осмотрите панель тегов и прочитайте хотя бы один документ на семейство шаблонов настоящей программой чтения с экрана. Диагностика ловит структурные ошибки; только человеческое ухо ловит порядок чтения, который технически корректен, но практически сбивает с толку. Оценочные сборки и полный справочник API разметки находятся на странице продукта losLab PDF Library for Delphi