Technický článek

Vytváření struktury tagovaných PDF v Delphi s PDF Library for Delphi

Přístupné PDF stojí na jediné struktuře, kterou viditelná stránka nikdy neukáže: na stromu struktury definovaném v ISO 32000-1 §14.7. Je to logická hierarchie nadpisů, odstavců, tabulek a obrázků, položená přes vykreslený obsah a namapovaná na standardní role skrze mapu rolí. Čtečka obrazovky čte tento strom, ne značky na stránce. Bez něj je vygenerovaná faktura, která vypadá bezvadně, sémanticky prázdná, protože obsahový stream zaznamenává pořadí kreslení a nic víc. Celková částka může být ohlášena dřív než jednotlivé položky, patička může zasáhnout doprostřed odstavce, tabulka položek se může sesypat do jediného nerozlišeného proudu slov. Náklady na to, aby k tomu nedošlo, jsou nakloněné ve váš prospěch. Vydávat strukturu už během kreslení jsou minuty kódu; dodělávat ji do hotových dokumentů je nápravný projekt. losLab PDF Library (PDF Library for Delphi) zpřístupňuje tento strom Delphi a C++Builderu malou sadou volání, která každou kreslicí operaci obalí její logickou rolí

Jak se označený obsah váže na strom struktury

Spolupracují dvě vrstvy. V obsahovém streamu jsou kreslicí operace uzavřené do sekvencí označeného obsahu, z nichž každá nese celočíselné MCID. V katalogu dokumentu mapuje strom struktury tato MCID do hierarchie typovaných prvků (H1, P, Table, Figure) s atributy, jako je alternativní text a jazyk. Vlastní typy prvků jsou přípustné, ale každý z nich se musí přes mapu rolí (ISO 32000-1 §14.8.4) vyřešit na standardní roli. Obsah, který nenese vůbec žádný význam, například linky, pozadí a opakovaný stránkový mobiliář, se označí jako artefakt, aby jej asistivní technologie přeskočila a nečetla uprostřed věty

PDF Library for Delphi udržuje obě vrstvy za jedinou dvojicí závorek. BeginTag otevře prvek struktury a zahájí sekvenci označeného obsahu, kreslicí volání dopadnou dovnitř a EndTag obojí uzavře. Účetnictví, na kterém ručně dělané tagování ztroskotává, tedy MCID, strom rodičů a odkazy na stránky, se odehrává uvnitř, kam nedosáhnete, a tak to nezkazíte

Diagram PDF Library for Delphi vážící běhy označeného obsahu s celočíselnými MCID na strom struktury H1, P a Figure přes mapu rolí, s artefakty vyloučenými z čtecího pořadí
Celočíselné MCID vážou úseky marked-content na typovaný strom struktury, zatímco role map řeší vlastní role a artefakty zůstávají mimo pořadí čtení

Práci rámují dva přepínače na úrovni dokumentu ještě dřív, než se otevře jakýkoli tag. SetMarkInfo zapíše do katalogu příznak deklarující dokument jako tagovaný a IsTaggedPDF jej přečte zpět, což je levná první sonda při rozhodování, zda příchozí soubor má vůbec nějakou strukturu, kterou by stálo za to zachovat. Jazyk má dva vstupní body. SetDocumentLanguage nastavuje výchozí jazyk dokumentu samostatně, zatímco SetPDFUAMode jej nastaví jako součást zapnutí plného výstupu PDF/UA. Soubor může být užitečně otagovaný, aniž by si nárokoval shodu s PDF/UA, a fázované zavádění často začíná právě tady

Tagujte během kreslení, ne potom

Generovací vzor, který funguje, spočívá v tom brát tagovou závorku jako součást hlavičky každého kreslicího volání, nikdy jako pozdější průchod:

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // počátek vlevo nahoře
    Lib.SetPDFUAMode('en-US');                 // zvedne verzi ukládání na PDF 1.7
    Lib.SetInformation(1, 'Service Manual');   // /Title je pro PDF/UA povinný
    Lib.AddRoleMap('ManualTitle', 'H1');       // vlastní typ -> standardní role
    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');               // dekorace stránky: mimo čtecí proud
    // ... zde vykreslete linky a podkladový tón ...
    Lib.EndArtifact;
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

Tři volání v té sekvenci mají váhu pro shodu. SetPDFUAMode zapíná výstup PDF/UA a tiše zvedá verzi dokumentu na PDF 1.7, což koliduje s připnutím verze. Dokument uzamčený na PDF 1.4 pomocí LockSaveVersion odmítne uložení a po zapnutí režimu UA vrátí chybový kód 602; tento střet vyplouvá obvykle tam, kde archivní profily a požadavky na přístupnost konfigurují různé týmy. SetInformation(1, ...) zapíše název dokumentu, o němž ISO 14289 očekává, že jej prohlížeče ukážou místo názvu souboru; jeho absence patří v praxi k nejčastějším nálezům PDF/UA. AddRoleMap registruje vlastní typ ManualTitle jako H1 a jeho vynechání znamená, že diagnostika popsaná níže označí nenamapovanou roli

Úrovně nadpisů si zaslouží záměrnou politiku, ne nahodilá rozhodnutí učiněná podle vzhledu stránky. Uživatelé čteček obrazovky skáčou mezi sekcemi zkratkou po nadpisech, takže šablona, jež jde z H1 rovnou na H3, protože mezistupeň vypadal ve vizuálním návrhu příliš velký, tuto navigaci tiše rozbíjí a žádná vizuální kontrola to nikdy nezachytí. Je to přesně ta vada, kvůli níž existuje diagnostika HEADING-LEVEL-SKIP. Namapujte vizuální styly každé šablony na pevný žebřík nadpisů jednou a na jednom místě a odchylka vůbec nezačne

Tabulky, v nichž se čtečka obrazovky skutečně vyzná

Nakreslené linky mřížky mimo obrazovku nic neznamenají. Čtečky obrazovky se orientují podle strukturních vztahů: které buňky jsou záhlaví, co které záhlaví řídí a jak se datové buňky váží na záhlaví v nepravidelných rozvrženích. Všechny tři obstarají volání pro atributy prvků struktury:

Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column');          // platí jen po dobu, kdy je toto TH otevřené
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2);               // záhlaví přesahuje sloupec hodnoty a jednotky
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part');      // explicitní vazba pro nepravidelné tabulky
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table

Pravidlo pořadí je přísné a vymáhané mlčky. Každé volání SetStructElem* se vztahuje k tagu, jenž je v ten okamžik otevřený, tedy mezi jeho BeginTag a EndTag, a vrací 0, aniž by cokoli vyhodilo, když žádný tag otevřený není nebo se atribut na ten aktuální nehodí. Špatně umístěné volání prostě zmizí. Obalení návratových hodnot do assertů během vývoje odchylku zachytí, dokud ji ještě vidíte; ponechána bez povšimnutí se chybějící oblast působnosti projeví teprve tehdy, až audit přístupnosti projede tabulku skutečnou čtečkou obrazovky. Identifikátory prvků předané přes BeginTagEx2 plní strom ID (ISO 32000-1 §14.7.4) a právě díky tomu je vazba SetStructElemHeaders vůbec rozluštitelná

Tatáž rodina atributů pokrývá i zbytek toho, o co se asistivní technologie opírá. SetStructElemListNumbering deklaruje, jak jsou položky seznamu označené, takže čtečka obrazovky ohlásí pozici v seznamu místo odříkávání znaků odrážek. SetStructElemBBox zaznamenává ohraničující obdélník obrázků a tabulek, který zobrazení s přeléváním textu používají k umístění obsahu. SetStructElemActualText dodává náhradní text pro úseky, jejichž glyfy se nemapují na čitelné znaky, například pro iniciálu složenou z vektorové kresby. Každý se řídí týmž pravidlem: naváže se na otevřený tag, nebo zmizí

Tabulkový diagram PDF Library for Delphi ukazující rozsah TH, colspan dva a atribut headers vážící se k datovým buňkám, vedle pravidla, že volání atributů se vážou jen, dokud je jejich tag otevřený
Čtečky obrazovky sledují vazby TH scope, colspan a headers místo vykreslených čar a volání atributů váže jen tehdy, když je jejich tag otevřený

Artefakty, jazyk a diagnostická brána před uložením

Opakovaný stránkový mobiliář, tedy průběžná záhlaví, značky pro skládání, vodoznaky a podkladové tóny, patří dovnitř závorek BeginArtifact a EndArtifact, aby nikdy nevstoupil do čtecího proudu. Jazyk se dědí. Výchozí hodnota dokumentu pochází z argumentu SetPDFUAMode a úsek v jiném jazyce ji přebije po jednotlivých prvcích přes BeginTagEx nebo SetStructElemLang. Právě to udrží francouzskou citaci uvnitř anglické příručky vyslovitelnou

Před uložením projede GetPDFUADiagnostics strukturní kontroly knihovny nad dokumentem v paměti a vrátí nálezy jako text, přičemž prázdný řetězec znamená, že se nic nenašlo. Kódy pojmenovávají klasické autorské chyby napřímo: FIGURE-NO-ALT pro obrázek bez alternativního textu, HEADING-LEVEL-SKIP pro H3 následující po H1, ROLEMAP-UNMAPPED pro vlastní typ, jenž nebyl nikdy zaregistrován. Zapojte to do sestavení (vygenerujte sadu dokumentů, při neprázdné diagnostice krok shoďte) a z regresí přístupnosti se stanou selhání ve stylu chyb překladu namísto auditních nálezů o měsíce později. Úplný verdikt o shodě stále patří preflightu nad uloženým souborem, jemuž se věnuje Preflight PDF/A a PDF/UA v Delphi, protože některé normalizace se uplatní až během serializace

Navigace v anotacích má vlastní páčku. PDF/UA očekává, že průchod formulářovými poli a odkazy z klávesnice bude následovat pořadí struktury, a SetTabOrderMode zapíše položku pořadí tabulátoru na úrovni stránky, kterou prohlížeče respektují, přičemž GetTabOrderMode je k dispozici pro audit příchozích souborů. Je to typ požadavku, jehož si nikdo nevšimne, dokud chybu nenahlásí uživatel pracující jen s klávesnicí, a jeho splnění stojí jedno volání na dokument

Stromy struktury nepřežijí každé slučování

Tagované dokumenty zůstanou tagované jen tehdy, když každý pozdější zpracovatelský krok strom zachová, a ostrou hranou uvnitř PDF Library for Delphi je rodina slučovacích seznamů. MergeFileListFast vyměňuje zachování stromu struktury za rychlost. U dávek naskenovaných obrázků je to správný obchod a u tagovaných zpráv špatný, protože výstup se otevře bez potíží, vykreslí se identicky a přitom tiše přišel o svou vrstvu přístupnosti. Kdykoli je kterýkoli vstup tagovaný, používejte výchozí MergeFileList nebo striktní variantu a zařaďte IsTaggedPDF mezi kontrolní tvrzení po kompletaci, aby zploštělá dávka nemohla odejít, aniž by si toho někdo všiml. Kompletační pipeline pro velké sady dokumentů nesou takových kompromisů víc a probírá je článek slučování, dělení a přímý přístup u velkých PDF

Diagram PDF Library for Delphi: GetPDFUADiagnostics vrací prázdný řetězec nebo pojmenované nálezy jako FIGURE-NO-ALT, jež nechají build propadnout dřív, než preflight posoudí uložený soubor
GetPDFUADiagnostics hlásí nálezy jako FIGURE-NO-ALT před uložením a neprázdný výsledek zařazený do buildu zfailuje krok okamžitě

Ověřovací smyčka se uzavírá mimo knihovnu: otevřete výstup v Acrobatu, prohlédněte si panel tagů a alespoň jeden dokument z každé rodiny šablon si přečtěte skutečnou čtečkou obrazovky. Diagnostika zachytí strukturní chyby; jen lidské ucho zachytí pořadí čtení, které je technicky platné a prakticky matoucí. Evaluační sestavení a kompletní referenční příručka tagovacího API jsou na produktové stránce losLab PDF Library for Delphi