Artykuł techniczny

Automatyczne tagowanie struktury dostępnego PDF w Delphi

Otwórz wygenerowany raport w czytniku ekranu, a typowe usterki pokażą się w ciągu minuty: „Strona 3 z 40” jest czytana na każdej stronie, logo firmy jest ogłaszane jako „grafika” i nic po tym, a tabela wychodzi jako jeden długi ciąg liczb bez nazw kolumn. PDF Library for Delphi omija wszystkie trzy dzięki automatycznemu tagowaniu struktury: zawołaj SetAutoTagMode raz, a zwykłe wywołania rysujące produkują akapity, nagłówki, ilustracje, tabele i pomijalne elementy oprawy strony w miarę rysowania dokumentu

Ręczna droga nadal istnieje. Możesz owijać każde wywołanie rysujące w BeginTag i EndTag, i dla dokumentów o nietypowej strukturze to właściwe narzędzie. Dla zwykłego raportu, faktury albo wyciągu ręczne tagowanie uzależnia jednak dostępność od tego, że nikt nigdy nie zapomni pary tagów — w każdej ścieżce kodu, która cokolwiek rysuje, przez cały czas życia produktu. Nie obstawalibyśmy na tym wymogu zgodności

Co włącza SetAutoTagMode

SetAutoTagMode przyjmuje maskę bitową i zwraca tryb obowiązujący przed wywołaniem. Każdy bit obejmuje jeden rodzaj treści:

  • AUTOTAG_TEXT (1): tekst staje się akapitem, albo nagłówkiem, gdy jeden został zarejestrowany
  • AUTOTAG_FURNITURE (2): bieżące nagłówki, stopki i numery stron stają się artefaktami, które czytnik pomija
  • AUTOTAG_FIGURE (4): narysowany obraz staje się ilustracją, albo artefaktem, gdy zadeklarowałeś go jako dekoracyjny
  • AUTOTAG_TABLE (8): narysowane tabele przenoszą swoje wiersze i komórki do drzewa struktury
  • AUTOTAG_DEFAULT (15): wszystkie cztery i wartość, którą rekomendujemy dla każdego nowego kodu raportów

Włączenie trybu oznacza też dokument jako otagowany i ten krok znaczy więcej, niż wygląda. Zgodnie z ISO 32000-1 §14.7.1 czytnik traktuje plik jako nieotagowany, chyba że słownik /MarkInfo w katalogu mówi /Marked true. Plik może nieść kompletne, poprawne drzewo struktury i mimo to być ogłaszany jako pozbawiony struktury w ogóle, bo brakuje jednej flagi, która każe technologii wspomagającej tego szukać. Drzewo tam jest. Nic go nie czyta

Tagowanie tekstu działa per wywołanie. Każde wywołanie DrawText, DrawWrappedText albo DrawTextBox staje się jednym elementem, więc akapit rysowany linia po linii pięcioma wywołaniami DrawText dociera do czytnika jako pięć jednolinijkowych akapitów. Jeśli twój kod układu sam łamie linie, warto to zmienić: przekaż cały akapit do DrawWrappedText i pozwól, by jedno wywołanie produkowało jeden akapit

Diagram masek bitowych SetAutoTagMode mapujących zwykłe wywołania rysujące Delphi na drzewo struktury otagowanego PDF, z flagą Marked wyróżnioną jako wymagana
Każdy aktywny bit SetAutoTagMode kieruje zwykłe wywołania rysujące do pasującego elementu drzewa struktury
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // początek w lewym górnym rogu, y rośnie w dół
    Lib.SetDocumentLanguage('en-US');
    Lib.SetAutoTagMode(AUTOTAG_DEFAULT);       // przed jakimkolwiek wywołaniem RegisterHeading
    Lib.AddStandardFont(4);

    Lib.SetTextSize(18);
    Lib.RegisterHeading(1, 'Annual service report');
    Lib.DrawText(72, 96, 'Annual service report');        // tagowane jako H1

    Lib.SetTextSize(11);
    Lib.DrawWrappedText(72, 130, 450,
      'Every unit installed before 2024 was inspected. ' +
      'Units that failed the torque check were serviced on site.');  // jeden P

    // ... pozostałe strony ...

    // Przechodzi po każdej istniejącej stronie, więc zawołaj raz, gdy strony są narysowane
    Lib.AddPageFooter('', 'Page &P of &N', 36, 1.0, 1);  // artefakt paginacji
    Lib.SaveToFile('report.pdf');
  finally
    Lib.Free;
  end;
end;

Komentarz w trzeciej linii jest tam nie bez powodu. RegisterHeading kolejkuje poziom nagłówka tylko wtedy, gdy tagowanie tekstu już działa, a SetAutoTagMode kasuje każdy skolejkowany poziom w chwili wykonania. Zarejestruj nagłówek najpierw, a tryb włącz potem, i tytuł wyjdzie jako zwykły akapit, bez żadnego błędu, który by ci to powiedział

Dlaczego nagłówek czeka przez obraz narysowany przed nim?

RegisterHeading nazywa poziom dla następnego kawałka tekstu i tylko tekst może go zużyć. Jeśli w międzyczasie zostanie narysowany obraz, obraz staje się ilustracją, a nagłówek czeka w kolejce na tekst, który przyjdzie potem. Oczywista implementacja pozwala każdemu tagowi skonsumować skolejkowany poziom, i wtedy typowa sekwencja — zarejestrowanie nagłówka sekcji, narysowanie obrazu banera i narysowanie tytułu — daje baner otagowany jako H2 i prawdziwy tytuł zdegradowany do akapitu. Czytelnik nawigujący po nagłówkach ląduje na obrazku

Z działania kolejki wynikają jeszcze dwie rzeczy. Poziom nagłówka przyczepia się do tekstu, który przyjdzie następny, niezależnie od łańcucha podanego do RegisterHeading: jeśli twój układ rysuje mały nadtytuł „Sekcja 3” nad tytułem, nadtytuł staje się nagłówkiem. I poziomy głębsze niż sześć są zapisywane jako H6, bo czytnik niczego poza tym nie zna. Argument Title nadal robi swoją robotę, zasilając konspekt, który DrawTOC zamienia potem w spis treści

Obrazy dostają tej samej obsługi „zużyte na jeden element”. RegisterFigure dostarcza opis, który poniesie następny obraz, a RegisterDecoration deklaruje następny obraz jako linię, obramowanie albo tło bez znaczenia, co jest potem zapisywane jako artefakt Layout (ISO 32000-1 §14.8.2.2). Każdą deklarację konsumuje dokładnie jeden obraz, więc późniejszy obraz nigdy nie odziedziczy opisu pisanego dla wcześniejszego. Tekst alt przyczepiony do niewłaściwego obrazka jest gorszy niż żaden, bo czytnik stwierdza coś fałszywego z pełnym przekonaniem

Celowo nie próbowaliśmy wnioskować, które obrazy są dekoracyjne. Obraz to albo treść, którą czytnik powinien opisać, albo treść, którą powinien pominąć, i tylko wołający kod wie, które. Przypadku trzeciego trzeba unikać: obraz narysowany bez żadnej deklaracji staje się ilustracją bez opisu, co wywala PDF/UA i każe czytnikowi ekranu zatrzymać się, powiedzieć „grafika” i potem nic. Jeśli przeszukasz kod raportu greppem za DrawImage, każde trafienie powinno mieć RegisterFigure albo RegisterDecoration tuż nad sobą

Diagram PDF Library for Delphi pokazujący zarejestrowany nagłówek PDF czekający przez wtrącony obraz, podczas gdy RegisterFigure i RegisterDecoration są każde zużywane na dokładnie jeden obraz
RegisterHeading czeka na następny tekst, cokolwiek innego zostałoby narysowane w międzyczasie
Lib.RegisterHeading(2, 'Gearbox inspection');   // skolejkowany na następny tekst

Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0);       // wczytuje i zaznacza obraz
Lib.DrawImage(72, 160, 360, 270);             // staje się otagowaną ilustracją Figure

Lib.RegisterDecoration;                       // bezsensowna linia
Lib.AddImageFromFile('divider.png', 0);
Lib.DrawImage(72, 450, 360, 4);               // rysowany wewnątrz artefaktu układu

Lib.DrawText(72, 480, 'Gearbox inspection');  // dopiero teraz pojawia się H2

Opis zasługuje na więcej starania niż jakikolwiek inny łańcuch w dokumencie. Czytelnik niewidzący dostaje go w miejsce obrazka i to wszystko, co dostaje. „Wykres” nie mówi mu nic; „Przychody kwartalne wg regionów, z regionem wschodnim najwyższym w Q3” mówi mu to, co widzący czytelnik wyciąga z obrazu w dwie sekundy

Tabele: wiersze nagłówka są deklarowane, nigdy zgadywane

Z włączonym bitem tabeli DrawTableRows zapisuje tabelę, jej wiersze i komórki do drzewa struktury, więc czytnik potrafi powiedzieć, w której kolumnie leży wartość, zamiast wykrzykiwać siatkę jako niezwiązany tekst. SetTableHeaderRowCount mówi, ile wiodących wierszy to nagłówki. Te wiersze są zapisywane jako komórki TH z zakresem kolumnowym, co pozwala czytnikowi ogłosić „Torque” przed wartością, do której użytkownik przeszedł

Liczba wierszy nagłówka domyślnie wynosi zero i to był świadomy wybór. Pierwszy wiersz tabeli zwykle jest nagłówkiem, więc zgadywanie byłoby trafne większością czasu. Koszt złego zgadnięcia jest jednak asymetryczny: wiersz danych wzięty za nagłówek jest czytany na głos przed każdą wartością w swojej kolumnie, co o wiele bardziej miesza niż tabela bez nagłówków w ogóle. Jedna linijka kodu od ciebie jest tańsza niż ten tryb awarii

Deklarowanie wierszy nagłówka i powtarzanie ich na każdej stronie to osobne decyzje. SetTableHeaderRowCount jest semantyczne i nie zmienia na stronie niczego. Powtarzanie to układ i należy do argumentu RepeatHeaderRows wywołania DrawTaggedTableRows. Gdyby obie rzeczy były jednym ustawieniem, żądanie poprawnych tagów nagłówkowych zaczęłoby też rysować powtarzane pasy nagłówkowe, o które nie prosiłeś, i układ sprawdzany strona po stronie by się przesunął. Wiersze podane jako RepeatHeaderRows też są tagowane jako nagłówki, więc nie musisz mówić tego samego dwa razy

Diagram PDF Library for Delphi: komórki nagłówkowe tabeli niosą zakres kolumnowy w drzewie struktury, podczas gdy RepeatHeaderRows trzyma powtarzanie przy łamaniu stron jako decyzję wyłącznie układową
Wiersze nagłówka mieszkają w drzewie struktury raz, z zakresem kolumnowym przyczepionym do każdej komórki TH
var
  TableID: Integer;
begin
  TableID := Lib.CreateTable(40, 3);
  Lib.SetTableHeaderRowCount(TableID, 1);       // wiersz 1 to pas nagłówka
  Lib.SetTableCellContent(TableID, 1, 1, 'Part');
  Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
  Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
  // ... wypełnij wiersze danych ...
  // Narysuj wiersze 1..40 w paśmie 600pt, powtarzając jeden wiersz nagłówka na stronę
  Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;

Ręcznie tagowane sekcje wewnątrz automatycznego dokumentu

Tagowanie automatyczne schodzi na bok, dopóki tag otwarty ręcznie jest wciąż otwarty. Pozwala to twemu kodowi samemu opisać część dokumentu i zostawić resztę bibliotece bez wzajemnego zagnieżdżania się obu, co jest układem, jakiego chcą większość prawdziwych dokumentów. Strona tytułowa i blok podpisów mają strukturę, którą rozumie tylko twój kod. Dwieście stron tekstu głównego między nimi — już nie

Trzy reguły utrzymują wynik poprawnym niezależnie od miksu, którego użyjesz. Nic nie jest tagowane wewnątrz artefaktu, bo sekwencja marked-content dla artefaktu nie może jednocześnie nieść elementu struktury, a plik, który spróbuje, wywala walidację PDF/UA. Pusty tekst nie otwiera żadnego elementu, więc zabłąkane DrawText z pustym łańcuchem nie zostawi po sobie elementu, który czytnik ogłaszałby jako pustkę. I wywołanie biblioteczne, które wewnątrz rysuje przez inne wywołania rysujące, otwiera jeden tag na zewnętrznym poziomie, więc jego kawałki nigdy nie lądują zagnieżdżone we własnym, drugim elemencie

Po zapisie przeczytaj drzewo z powrotem, zanim mu zaufasz. API odczytu jest indeksowane od 1, co nas zaskoczyło: nasza pierwsza pętla kontrolna chodziła od 0 do StructTreeItemCount - 1, dostała pusty rekord na starcie, nigdy nie dosięgła ostatniego elementu, i przez chwilę byliśmy przekonani, że jakiś akapit zniknął. Nic nie znikło. Pętla była po prostu o jeden off na obu końcach

var
  I: Integer;
begin
  // Cytat blokowy otagowany ręcznie; tagowanie automatyczne trzyma się od niego z dala
  Lib.BeginTag('BlockQuote', '', '');
  Lib.DrawWrappedText(72, 520, 450,
    'All readings were taken with the unit at operating temperature.');
  Lib.EndTag;
  Lib.SaveToFile('report.pdf');

  // Przeczytaj drzewo z powrotem: indeksy biegną od 1 do StructTreeItemCount
  if Lib.LoadFromFile('report.pdf', '') = 1 then
    for I := 1 to Lib.StructTreeItemCount do
      Writeln(Lib.GetStructTreeItemText(I, 2), '  ',    // typ po rozwiązaniu
              Lib.GetStructTreeItemText(I, 4));         // /Alt, jeśli jest
end;

Taki wypis to szybka kontrola sensowności, nie werdykt. Chcesz zobaczyć H1 przed pierwszym akapitem, każdą ilustrację niosącą swój opis i żadną ilustrację z pustym /Alt

Gdy kolejność rysowania i kolejność czytania się rozchodzą

Tagowanie automatyczne zapisuje elementy w kolejności rysowania treści. Jeśli twój kod układu rysuje pasek boczny przed treścią główną, to jest kolejność, którą trzyma drzewo, i to jest kolejność, którą czyta czytnik ekranu. Dla dokumentów, gdzie kolejność wizualna i kolejność czytania naprawdę się różnią, albo które potrzebują innych typów struktury albo języka per element, lepszym dopasowaniem jest API tagowania ręcznego, a przewodnik po tagowanym PDF i strukturze dostępności omawia role, zakresy i powiązania nagłówków w szczegółach

Dla stron, których sam nie rysowałeś — na przykład importowanych bez marked content — AutoTagPages działa po fakcie: bierze medianę rozmiaru tekstu na podanych stronach jako tekst główny, traktuje wyraźnie większe biegi jako nagłówki i oznacza pozostałe malowanie jako artefakty. To właściwe narzędzie dla treści, których nie da się wygenerować od nowa. Dla wszystkiego, co rysuje twój własny kod, wolimy tagowanie w czasie rysowania, bo tam bibliotece mówi się, czym jest nagłówek, zamiast zgadywać z rozmiaru fontu

Gdy dokument jest gotowy, przepuść go przez checker zamiast zakładać: notatki o preflight PDF/A i PDF/UA pokazują, jak dostać werdykt o strukturze, którą wyprodukowałeś, a artykuł o eksporcie raportów napędzanym datasetem pokazuje, gdzie te wywołania siedzą w silniku raportów budującym układ z danych

Ponieważ PDF Library for Delphi jest napisana w natywnym Pascalu dla Delphi, C++Buildera i Lazarusa, bez zewnętrznego runtime'u PDF, który trzeba dołączać, otagowany wynik pochodzi z tego samego kodu, który rysuje stronę. Kompletne API i listę platform znajdziesz na stronie produktu PDF Library for Delphi