Artykuł techniczny

Drzewa struktury oznaczonego PDF w Delphi

Dostępny plik PDF opiera się na jednej strukturze, której widoczna strona nigdy nie pokazuje: na drzewie struktury zdefiniowanym w ISO 32000-1 §14.7. To logiczna hierarchia nagłówków, akapitów, tabel i rysunków, nałożona na namalowaną treść i odwzorowana na standardowe role przez mapę ról. Czytnik ekranu czyta to drzewo, a nie ślady na stronie. Bez niego wygenerowana faktura, która wygląda nieskazitelnie, jest semantycznie pusta, ponieważ strumień treści zapisuje kolejność rysowania i nic poza tym. Suma może zostać odczytana przed pozycjami, stopka może wciąć się w akapit, a tabela pozycji może zwinąć się w jeden nierozróżnialny ciąg słów. Koszt zapobieżenia temu jest przechylony na twoją korzyść. Emitowanie struktury w trakcie rysowania to minuty kodu; doszywanie jej do gotowych dokumentów to projekt naprawczy. losLab PDF Library (PDF Library for Delphi) udostępnia to drzewo Delphi i C++Builder przez niewielki zestaw wywołań, które opakowują każdą operację rysowania w jej logiczną rolę

Jak treść znakowana wiąże się z drzewem struktury

Współpracują dwie warstwy. W strumieniu treści operacje rysowania są ujęte w sekwencje treści znakowanej, z których każda niesie całkowitoliczbowy MCID. W katalogu dokumentu drzewo struktury odwzorowuje te MCID na hierarchię typowanych elementów (H1, P, Table, Figure) z atrybutami takimi jak tekst alternatywny i język. Własne typy elementów są dozwolone, ale każdy musi rozwiązywać się do standardowej roli przez mapę ról (ISO 32000-1 §14.8.4). Treść, która w ogóle nie niesie znaczenia, jak linie, tła i powtarzalne wyposażenie strony, jest oznaczana jako artefakt, żeby technologia wspomagająca ją pomijała zamiast czytać w połowie zdania

PDF Library for Delphi utrzymuje obie warstwy za jedną parą klamer. BeginTag otwiera element struktury i rozpoczyna sekwencję treści znakowanej, wywołania rysowania lądują w środku, a EndTag zamyka oba. Księgowość, na której potyka się ręcznie pisane oznaczanie, czyli MCID, drzewo rodziców i odwołania do stron, dzieje się wewnętrznie, tam gdzie nie da ci się jej zepsuć

Diagram PDF Library for Delphi wiążący przebiegi treści znakowanej niosące całkowitoliczbowe MCID z drzewem struktury H1, P i Figure poprzez mapę ról, z artefaktami wyłączonymi z kolejności odczytu
Całkowitoliczbowe MCID wiążą przebiegi treści znakowanej z typowanym drzewem struktury, mapa ról rozwiązuje role własne, a artefakty pozostają poza kolejnością odczytu

Dwa przełączniki na poziomie dokumentu obramowują pracę, zanim otworzy się jakikolwiek znacznik. SetMarkInfo zapisuje flagę katalogu deklarującą dokument jako oznaczony, a IsTaggedPDF odczytuje ją z powrotem, co jest tanią pierwszą sondą przy rozstrzyganiu, czy przychodzący plik ma jakąkolwiek strukturę wartą zachowania. Język ma dwa punkty wejścia. SetDocumentLanguage ustawia domyślną wartość dokumentu samodzielnie, natomiast SetPDFUAMode ustawia ją w ramach włączania pełnego wyjścia PDF/UA. Plik może być użytecznie oznaczony bez deklarowania zgodności z PDF/UA, a etapowe wdrożenie często zaczyna się właśnie tam

Oznaczanie w trakcie rysowania, a nie po fakcie

Wzorzec generowania, który działa, polega na traktowaniu klamry znacznika jak części sygnatury każdego wywołania rysującego, nigdy jako późniejszego przebiegu:

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // początek układu w lewym górnym rogu
    Lib.SetPDFUAMode('en-US');                 // podnosi wersję zapisu do PDF 1.7
    Lib.SetInformation(1, 'Service Manual');   // /Title jest obowiązkowy dla PDF/UA
    Lib.AddRoleMap('ManualTitle', 'H1');       // typ własny -> rola standardowa
    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');               // dekoracja strony: wyłączona z odczytu
    // ... narysuj linie i tło ...
    Lib.EndArtifact;
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

Trzy wywołania w tej sekwencji niosą ciężar zgodności. SetPDFUAMode włącza wyjście PDF/UA i po cichu podnosi wersję dokumentu do PDF 1.7, co koliduje z przypinaniem wersji. Dokument zablokowany na PDF 1.4 przez LockSaveVersion odmawia zapisu i zwraca kod błędu 602, gdy tryb UA jest aktywny; to zderzenie ujawnia się zwykle wtedy, gdy profile archiwalne i wymogi dostępności konfigurują różne zespoły. SetInformation(1, ...) zapisuje tytuł dokumentu, który zgodnie z ISO 14289 przeglądarki mają pokazywać zamiast nazwy pliku; jego brak to jedno z najczęstszych ustaleń PDF/UA spotykanych w praktyce. AddRoleMap rejestruje własny typ ManualTitle jako H1, a pominięcie tego sprawia, że opisana niżej diagnostyka zgłasza nieodwzorowaną rolę

Poziomy nagłówków zasługują na przemyślaną politykę, a nie na doraźne wybory podyktowane wyglądem strony. Użytkownicy czytników ekranu skaczą między sekcjami skrótem do nagłówków, więc szablon przechodzący z H1 do H3, ponieważ poziom pośredni wyglądał w projekcie wizualnym na zbyt duży, po cichu psuje tę nawigację, a żaden przegląd wizualny nigdy tego nie wychwyci. To dokładnie ta wada, którą ma nazywać diagnostyka HEADING-LEVEL-SKIP. Odwzoruj style wizualne każdego szablonu na stałą drabinę nagłówków raz, w jednym miejscu, a dryf nigdy się nie zacznie

Tabele, po których czytnik ekranu faktycznie potrafi nawigować

Narysowane linie siatki nic nie znaczą poza ekranem. To, po czym nawigują czytniki ekranu, to relacje strukturalne: które komórki są nagłówkami, czym rządzi każdy nagłówek i jak komórki danych wiążą się z nagłówkami w nieregularnych układach. Wywołania atrybutów elementu struktury obsługują wszystkie trzy:

Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column');          // ważne tylko, dopóki ten TH jest otwarty
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2);               // nagłówek obejmuje kolumnę wartości i jednostki
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part');      // jawne wiązanie dla tabel nieregularnych
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table

Reguła kolejności jest ścisła i egzekwowana w milczeniu. Każde wywołanie SetStructElem* odnosi się do znacznika otwartego w tej właśnie chwili, między jego BeginTag a EndTag, i zwraca 0 bez zgłaszania czegokolwiek, gdy żaden znacznik nie jest otwarty albo atrybut nie ma zastosowania do bieżącego. Źle umieszczone wywołanie po prostu znika. Opakowanie wartości zwracanych w asercje w czasie pracy nad kodem wychwytuje dryf, dopóki jeszcze go widzisz; zostawione samo sobie brakujące ustawienie zakresu ujawnia się dopiero wtedy, gdy audyt dostępności przejedzie przez tabelę prawdziwym czytnikiem ekranu. Identyfikatory elementów przekazywane przez BeginTagEx2 zasilają drzewo identyfikatorów (ISO 32000-1 §14.7.4), i to właśnie sprawia, że wiązanie SetStructElemHeaders w ogóle daje się rozwiązać

Ta sama rodzina atrybutów pokrywa resztę tego, na czym opiera się technologia wspomagająca. SetStructElemListNumbering deklaruje, jak etykietowane są elementy listy, więc czytnik ekranu ogłasza pozycję na liście, zamiast recytować glify punktorów. SetStructElemBBox zapisuje prostokąt ograniczający rysunków i tabel, którego widoki z przepływem używają do rozmieszczenia treści. SetStructElemActualText dostarcza tekst zastępczy dla przebiegów, których glify nie odwzorowują się na czytelne znaki, jak inicjał złożony z grafiki wektorowej. Każde z nich podlega tej samej regule: wiąże się z otwartym znacznikiem albo znika

Diagram tabeli PDF Library for Delphi pokazujący zakres TH, scalenie dwóch kolumn i atrybut headers wiążący komórki danych, obok reguły, że wywołania atrybutów wiążą się tylko wtedy, gdy ich znacznik jest otwarty
Czytniki ekranu idą za zakresem TH, scaleniem kolumn i wiązaniami headers, a nie za narysowanymi liniami, a wywołania atrybutów wiążą się tylko przy otwartym znaczniku

Artefakty, język i bramka diagnostyki przed zapisem

Powtarzalne wyposażenie strony, czyli żywe paginy, znaczniki złamu, znaki wodne i tła, należy do klamer BeginArtifact i EndArtifact, żeby nigdy nie weszło do strumienia odczytu. Język jest dziedziczony. Domyślna wartość dokumentu pochodzi z argumentu SetPDFUAMode, a przebieg w innym języku nadpisuje ją dla pojedynczego elementu przez BeginTagEx albo SetStructElemLang. To właśnie utrzymuje francuski cytat wewnątrz angielskiej instrukcji w formie możliwej do wymówienia

Przed zapisem GetPDFUADiagnostics przepuszcza strukturalne kontrole biblioteki przez dokument w pamięci i zwraca ustalenia jako tekst, gdzie pusty łańcuch oznacza brak znalezisk. Kody nazywają klasyczne błędy autorskie wprost: FIGURE-NO-ALT dla obrazu bez tekstu alternatywnego, HEADING-LEVEL-SKIP dla H3 następującego po H1, ROLEMAP-UNMAPPED dla typu własnego, którego nigdy nie zarejestrowano. Wepnij to w budowanie (wygeneruj zestaw dokumentów, przerwij krok przy niepustej diagnostyce), a regresje dostępności staną się awariami w stylu błędu kompilacji zamiast ustaleń audytu za kilka miesięcy. Pełny werdykt zgodności nadal należy do preflightu na zapisanym pliku, opisanego w artykule o preflight PDF/A i PDF/UA w Delphi, ponieważ część normalizacji stosuje się dopiero podczas serializacji

Diagram PDF Library for Delphi przedstawiający GetPDFUADiagnostics zwracające pusty łańcuch albo nazwane ustalenia takie jak FIGURE-NO-ALT, które przerywają budowanie, zanim preflight oceni zapisany plik
GetPDFUADiagnostics zgłasza ustalenia takie jak FIGURE-NO-ALT przed zapisem, a niepusty wynik wpięty w budowanie natychmiast przerywa krok

Nawigacja po adnotacjach ma własne pokrętło. PDF/UA oczekuje, że przechodzenie klawiaturą po polach formularza i odsyłaczach będzie zgodne z kolejnością struktury, a SetTabOrderMode zapisuje wpis kolejności tabulacji na poziomie strony, który przeglądarki honorują, przy czym GetTabOrderMode służy do audytu plików przychodzących. To rodzaj wymagania, którego nikt nie zauważa, dopóki użytkownik korzystający wyłącznie z klawiatury nie zgłosi błędu, a spełnienie go kosztuje jedno wywołanie na dokument

Drzewa struktury nie przeżywają każdego scalania

Oznaczone dokumenty pozostają oznaczone tylko wtedy, gdy każdy późniejszy krok przetwarzania zachowuje drzewo, a ostrą krawędzią wewnątrz PDF Library for Delphi jest rodzina scalania list. MergeFileListFast wymienia zachowanie drzewa struktury na szybkość. To właściwa wymiana dla partii skanowanych obrazów i niewłaściwa dla oznaczonych raportów, ponieważ wyjście otwiera się bez zarzutu, renderuje identycznie i po cichu straciło swoją warstwę dostępności. Używaj domyślnego MergeFileList albo wariantu ścisłego zawsze, gdy którekolwiek wejście jest oznaczone, i uczyń IsTaggedPDF częścią asercji po złożeniu, żeby spłaszczona partia nie mogła wyjść bez niczyjej wiedzy. Potoki składania dużych zestawów dokumentów niosą więcej kompromisów tego rodzaju, omówionych w artykule o scalaniu, dzieleniu i bezpośrednim dostępie do dużych plików PDF

Pętla weryfikacji domyka się poza biblioteką: otwórz wyjście w Acrobacie, obejrzyj panel znaczników i przeczytaj co najmniej jeden dokument z każdej rodziny szablonów prawdziwym czytnikiem ekranu. Diagnostyka wychwytuje błędy strukturalne; tylko ludzkie ucho wychwyci kolejność odczytu technicznie prawidłową, a praktycznie zagmatwaną. Wersje ewaluacyjne i pełne odniesienie do API oznaczania znajdziesz na stronie produktu losLab PDF Library for Delphi