Технічна стаття

Створення структурних дерев тегованого PDF у Delphi за допомогою PDF Library for Delphi

Доступний 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, батьківське дерево й посилання на сторінки — відбувається внутрішньо, там, де ви не можете помилитися

Діаграма PDF Library for Delphi: прогони розміченого вмісту з цілими MCIDs прив'язуються через карту ролей до дерева структури H1, P і Figure, а артефакти виключаються з порядку читання
Цілочисельні 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, живлять дерево ID (ISO 32000-1 §14.7.4), і саме це робить прив'язку SetStructElemHeaders взагалі можливою для розв'язання

Та сама родина атрибутів охоплює решту того, на що спирається допоміжна технологія. SetStructElemListNumbering оголошує, як позначені елементи списку, тож програма читання з екрана оголошує позицію в списку замість того, щоб зачитувати гліфи маркерів. SetStructElemBBox фіксує обмежувальний прямокутник рисунків і таблиць, який використовують режими перекомпонування для розміщення вмісту. SetStructElemActualText надає текст-заміну для фрагментів, чиї гліфи не зіставляються з читабельними символами, як-от буквиця, зібрана з векторної графіки. Кожен з них підпорядковується тому самому правилу: він прив'язується до відкритого тегу — або зникає

Таблична діаграма PDF Library for Delphi: область TH, colspan два і атрибут headers, що прив'язуються до комірок даних; поруч правило, що виклики атрибутів прив'язуються лише поки їхній тег відкритий
Екранні читачі йдуть за scope, colspan і прив'язками headers у TH замість накреслених ліній, а виклики атрибутів прив'язуються лише доки їхній тег відкритий

Артефакти, мова та шлюз діагностики перед збереженням

Повторювані елементи оформлення сторінки, тобто наскрізні колонтитули, мітки згину, водяні знаки й тло, мають перебувати всередині дужок BeginArtifact і EndArtifact, щоб ніколи не потрапляти до потоку читання. Мова успадковується. Типова мова документа береться з аргументу SetPDFUAMode, а фрагмент іншою мовою перевизначає її для окремого елемента через BeginTagEx або SetStructElemLang. Саме це робить французьку цитату всередині англомовного посібника вимовною

Перед збереженням GetPDFUADiagnostics проганяє структурні перевірки бібліотеки над документом у пам'яті й повертає знахідки як текст, де порожній рядок означає, що нічого не знайдено. Коди прямо називають класичні помилки авторів: FIGURE-NO-ALT для зображення без альтернативного тексту, HEADING-LEVEL-SKIP для H3, що йде одразу за H1, ROLEMAP-UNMAPPED для власного типу, який так і не зареєстрували. Вбудуйте це у збірку (згенеруйте набір документів, провалюйте крок при непорожній діагностиці) — і регресії доступності стають помилками на кшталт помилок компіляції замість зауважень аудиту через місяці. Остаточний вердикт про відповідність усе одно належить preflight-перевірці збереженого файлу, що розглянута в розділі про preflight PDF/A та PDF/UA у Delphi, бо деякі нормалізації застосовуються лише під час серіалізації

Навігація анотаціями має власний важіль. PDF/UA очікує, що обхід полів форми та посилань з клавіатури відбуватиметься за порядком структури, і SetTabOrderMode записує запис порядку табуляції на рівні сторінки, який шанують переглядачі, а GetTabOrderMode доступний для аудиту вхідних файлів. Це та вимога, якої ніхто не помічає, доки користувач, що працює лише з клавіатури, не заведе баг, а виправити її коштує один виклик на документ

Структурні дерева переживають не кожне злиття

Тегований документ лишається тегованим лише тоді, коли кожен наступний крок обробки зберігає дерево, і гострий край усередині PDF Library for Delphi — це родина функцій merge-list. MergeFileListFast міняє збереження структурного дерева на швидкість. Це правильний компроміс для пакетів сканованих зображень і неправильний для тегованих звітів, бо результат відкривається нормально, відображається ідентично й тихцем втрачає свій шар доступності. Використовуйте типовий MergeFileList або строгий варіант щоразу, коли хоч один вхідний файл тегований, і зробіть IsTaggedPDF частиною перевірок після складання, щоб сплющений пакет не міг вийти в реліз непоміченим. Конвеєри складання для великих наборів документів несуть більше подібних компромісів, розглянутих у розділі про злиття, розділення та прямий доступ до великих PDF

Діаграма PDF Library for Delphi: GetPDFUADiagnostics повертає порожній рядок або іменовані знахідки, як-от FIGURE-NO-ALT, що провалюють збірку, перш ніж preflight винесе вердикт про збережений файл
GetPDFUADiagnostics повідомляє знахідки на кшталт FIGURE-NO-ALT ще до збереження, і непорожній результат, зшитий у збірку, негайно валить крок

Цикл перевірки замикається поза бібліотекою: відкрийте результат в Acrobat, огляньте панель тегів і прочитайте хоча б один документ з кожної родини шаблонів справжньою програмою читання з екрана. Діагностика ловить структурні помилки; лише людське вухо вловлює порядок читання, який технічно коректний, але практично спантеличує. Оцінні збірки та повний довідник API тегування розміщені на сторінці продукту losLab PDF Library for Delphi