Technický článek

Automatické značkování struktury pro přístupné PDF v Delphi

PDFlibPas umí dokument značkovat už během kreslení. Zapněte SetAutoTagMode a obyčejné volání DrawText se stane odstavcem, text nakreslený hned po RegisterHeading se stane nadpisem dané úrovně, běžící záhlaví a zápatí se stanou artefakty, které čtečka přeskočí, obrázky se stanou figurami a DrawTableRows přenese tabulku, její řádky i buňky do stromu struktury

Alternativou — a donedávna jedinou možností — bylo ručně obalovat každou kreslící volání do BeginTag a EndTag. To funguje a pro dokumenty s neobvyklou strukturou je to stále ten správný nástroj. U běžné sestavy, faktury či výpisu to ale znamená, že přístupnost výstupu závisí na tom, že někdo na každé větvi kódu, která něco kreslí, nikdy nezapomene pár značek

Co pokrývají bity režimu

SetAutoTagMode přijímá bitovou masku a vrací režim, který platil dosud. AUTOTAG_TEXT (1) značkuje text jako odstavec nebo jako nadpis, zrovna-li to přijde. AUTOTAG_FURNITURE (2) označuje běžící záhlaví, zápatí a čísla stránek za artefakty. AUTOTAG_FIGURE (4) mění kreslený obrázek na figuru, nebo na artefakt, byl-li deklarován jako dekorativní. AUTOTAG_TABLE (8) přenáší kreslené tabulky do stromu struktury. AUTOTAG_DEFAULT je 15, tedy všech čtyři dohromady

Zapnutí režimu navíc označí dokument jako značkovaný a tento krok je méně kosmetický, než zní. Čtečka pokládá dokument za neznačkovaný, pokud katalog neříká jinak (ISO 32000-1 §14.7.1), takže soubor, který nese kompletní strom struktury bez deklarace /MarkInfo, asistivní technologie ohlásí jako dokument bez struktury. Strom tam je, jen si ho nikdo nepřečte

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;

Odkud nadpis ví, ke kterému textu patří?

RegisterHeading určuje úroveň pro následující kreslený text a čeká, až se něco nakreslí. Přijde-li mezi tím obrázek, ten se stane figurou a nadpis zůstane čekat na text, který přijde po něm. Chování je záměrné: alternativa, kde by obrázek převzal úroveň nadpisu, vedla k dokumentům, ve kterých se dekorativní linka pod titulkem ohlašovala jako titulek

Stejné pravidlo „spotřebováno jednou položkou" platí pro figury. RegisterFigure dodává popis, který ponese následující obrázek, a RegisterDecoration deklaruje další obrázek jako linku, rámeček či pozadí bez významu. Obojí se spotřebuje jedním obrázkem, takže pozdější obrázek nikdy nedědí popis určený dřívějšímu — a právě tak v ručně značkovaném kódu končí alt text připojený k špatnému obrázku

Popis má větší váhu než kterýkoli jiný jediný řetězec v přístupném dokumentu. Nevidomý čtenář dostane popis místo obrázku a dostane jen jej. „Graf" není popis; „Čtvrtletní tržby podle regionů, přičemž východní region je nejvyšší ve Q3" ano

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

Tabulky, záhlaví a kde bydří rozhodnutí o opakování

Se zapnutým bitem tabulky přenáší DrawTableRows tabulku, její řádky i buňky do stromu struktury, takže čtenář dokáže říct, ve kterém sloupci hodnota stojí, místo aby celou tabulku přečetl jako sled nesouvisejících textů. SetTableHeaderRowCount určuje, kolik úvodních řádků je záhlaví; ty se zapíší jako buňky záhlaví nesoucí sloupcový scope, a právě to umožňuje čtečce ohlásit nadpis hodnoty, na níž se uživatel nachází

Řádky záhlaví určené tímto způsobem zůstávají tam, kde jsou. Jejich opakování v záhlaví každé stránky je rozhodnutí o layoutu a tím i zůstává: DrawTaggedTableRows má parametr RepeatHeaderRows přesně pro tento účel. Udržet obojí odděleně brání tomu, aby strom struktury získal druhou kopii záhlaví pro každý zalomení stránky, což by automatické opakování vyrobilo

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;

Kombinování automatického a ručního značkování

Automatické značkování se odklání stranou uvnitř značky otevřené ručně. Část dokumentu může popsat váš kód a zbytek přenechat knihovně, aniž by se do sebe dvě vrstvy zanořovaly — přesně takové uspořádání chce většina skutečných dokumentů. Obálka a blok podpisů mají strukturu, které rozumíte jen vy; dvě stě stran vlastního textu uprostřed nikoliv

Dvě bezpečnostní pravidla udržují výstup čistý. Uvnitř artefaktu se nic neznačkuje, protože obsah označený jako artefact nesmí nést žádný prvek struktury. A prázdný text neotevírá žádný prvek, takže úlet v podobě DrawText s prázdným řetězcem nemůže vyrobit prvek struktury, jejž by čtečka ohlásila jako prázdný. Obojí jsou vady, které ručně značkované dokumenty potichu hromadí a validátor je nahlásí hromadně až o měsíce později

Co automatické značkování stále nerozhoduje za vás

Pořadí čtení přesahující pořadí kreslení, sémantické role, jež nejsou odstavec, nadpis, figura ani tabulka, a deklarace jazyka. Automatické značkování přiřazuje strukturu v pořadí, v jakém se obsah kreslí — pokud váš kód layoutu nakreslí nejprve postranní panel a pak tělo textu, přesně takové pořadí si strom zaznamená. U dokumentů, kde se vizuální pořadí a pořadí čtení skutečně liší, zůstává ruční API značkování tím správným nástrojem a průvodce značkovaným PDF a strukturou přístupnosti rozebírá role, scope a vazby záhlaví do hloubky

Až je dokument hotový, raději validujte, než abyste předpokládali: poznámky k preflightu PDF/A a PDF/UA ukazují, jak získat verdikt nad vyrobenou strukturou, a průvodce exportem sestav řízeným daty popisuje, kam tyto volání zapadají v reportovacím enginu, který generuje layout z dat

PDFlibPas je nativní Pascal PDF knihovna pro Delphi, C++Builder a Lazarus bez externího PDF runtime, takže přístupný výstup produkuje stejný kód, který kreslí dokument — viz stránka produktu PDFlibPas pro úplné API a seznam platforem