Technical Article

Building Tagged PDF Structure Trees in Delphi with PDFlibPas

Dostopen dokument PDF temelji na strukturi, ki je vidna stran nikoli ne prikaže: strukturnem drevesu, določenem v standardu ISO 32000-1 §14.7. Gre za logično hierarhijo naslovov, odstavkov, tabel in slik, ki leži nad izrisano vsebino in je preslikana v standardne vloge prek mape vlog (Role Map). Bralnik zaslona bere to drevo, ne pa oznak na sami strani. Brez tega je ustvarjen račun, ki je vizualno brezhiben, semantično povsem prazen, saj tok vsebine beleži le vrstni red risanja in nič drugaga. Končni znesek je lahko prebran pred posameznimi postavkami, noga strani lahko prekine odstavek, tabela postavk pa se lahko zruši v en sam nerazločljiv niz besed. Preprečevanje tega je izjemno preprosto in se vam vsekakor obrestuje. Ustvarjanje strukture med risanjem zahteva le nekaj vrstic kode, medtem ko je naknadno dodajanje strukture v že končane dokumente zamuden projekt sanacije. Knjižnica losLab PDF Library (PDFlibPas) izpostavlja to drevo za Delphi in C++Builder prek majhne skupine klicev, ki vsako operacijo risanja ovijejo v njeno logično vlogo.

Kako se označena vsebina povezuje s strukturnim drevesom

Pri tem sodelujeta dve plasti. V toku vsebine so operacije risanja uokvirjene v zaporedja označene vsebine, pri čemer vsako nosi celoštevilski MCID. V katalogu dokumenta strukturno drevo preslika te MCID-je v hierarhijo tipiziranih elementov (H1, P, Table, Figure) z atributi, kot sta nadomestno besedilo in jezik. Uporaba lastnih tipov elementov je dovoljena, vendar se mora vsak od them prek mape vlog (ISO 32000-1 §14.8.4) razrešiti v standardno vlogo. Vsebina, ki nima nobenega pomena, kot so črte, ozadja in ponavljajoči se elementi strani, je označena kot artefakt, tako da jo podporna tehnologija preskoči, namesto da bi jo prebrala sredi stavka.

PDFlibPas vzdržuje obe plasti znotraj enega para oklepajev. Funkcija BeginTag odpre strukturni element in začne zaporedje označene vsebine, klici risanja se izvedejo znotraj njega, funkcija EndTag va zapre oba. Vodenje evidenc, ki običajno povzroča težave pri ročnem označevanju (MCID-ji, starševsko drevo in sklici na strani), poteka interno, kjer ne more priti do napak.

Dve stikali na ravni dokumenta določata okvir dela, preden se odpre katera koli oznaka. SetMarkInfo zapise zastavico kataloga, ki označuje dokument kot označen, IsTaggedPDF pa jo prebere nazaj, kar je hitra prva analiza pri odločanju, ali ima prejeti dokument kakšno strukturo, ki jo je vredno ohraniti. Jezik ima dve vstopni točki. SetDocumentLanguage samostojno nastavi privzeti jezik dokumenta, medtem ko ga SetPDFUAMode nastavi kot del omogočanja celotnega izvoza PDF/UA. Dokument je lahko koristno označen tudi brez zahteve po skladnosti s PDF/UA, in postopen uvajanje se pogosto začne ravno tam.

Označevanje med risanjem in ne naknadno

Učinkovit vzorec generiranja je, da oklepaj oznake obravnavate kot del podpisa vsakega klica risanja, nikoli pa kot naknaden korak:

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // top-left origin
    Lib.SetPDFUAMode('en-US');                 // bumps the save version to PDF 1.7
    Lib.SetInformation(1, 'Service Manual');   // /Title is mandatory for PDF/UA
    Lib.AddRoleMap('ManualTitle', 'H1');       // custom type -> standard 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');               // page decoration: excluded from reading
    // ... draw rules and background tint ...
    Lib.EndArtifact;
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

Trije klici v tem zaporedju imajo pomembno vlogo pri skladnosti. SetPDFUAMode omogoči izpis PDF/UA in tiho dvigne različico dokumenta na PDF 1.7, kar lahko trči z zaklepanjem različic. Dokument, zaklenjen na PDF 1.4 s funkcijo LockSaveVersion, se zavrne in vrne napako s kodo 602, ko je aktiven način UA, ta konflikt pa se pogosto pojavi, ko arhivske profile in zahteve glede dostopnosti konfigurirajo različne ekipe. Funkcija SetInformation(1, ...) zapise naslov dokumenta, za katerega standard ISO 14289 expects bralniki prikažejo namesto imena datoteke; njegova odsotnost je ena najpogostejših napak PDF/UA v praksi. Funkcija AddRoleMap registrira po meri ustvarjen tip ManualTitle kot H1, če jo preskočite, pa bo diagnostika, opisana spodaj, označila nepreslikano vlogo.

Ravni naslovov si zaslužijo premišljeno načrtovanje in ne priložnostnih odločitev na podlagi vizualnega izgleda strani. Uporabniki bralnikov zaslona se med razdelki premikajo s bližnjicami za naslove, zato predloga, ki gre z ravni H1 neposredno na H3 (ker je bila vmesna raven vizualno prevelika), tiho pokvari to navigacijo, česar pa vizualni pregled ne bo nikoli odkril. To je natanko tista napaka, ki jo označuje diagnostična koda HEADING-LEVEL-SKIP. Preslikajte vizualne sloge vsake predloge v fiksno lestvico naslovov enkrat in na enem mestu, pa do takšnih odstopanj ne bo prihajalo.

Tabele, ki jih bralnik zaslona dejansko lahko upravlja

Izrisane mrežne črte na zaslonu ne pomenijo ničesar. Bralniki zaslona krmarijo po strukturnih odnosih: katere celice so glave, kaj posamezna glava upravlja in kako se podatkovne celice povezujejo z glavami v nepravilnih postavitvah. Klici atributov strukturnih elementov urejajo vse tri primere:

Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column');          // valid only while this TH is open
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2);               // header spans the value and unit columns
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part');      // explicit binding for irregular tables
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table

Pravilo o vrstnem redu je strogo in se izvaja tiho. Vsak klic SetStructElem* se nanaša na oznako, ki je odprta v tistem trenutku (med BeginTag in EndTag). Če nobena oznaka ni odprta ali se atribut ne nanaša na trenutno oznako, vrne vrednost 0 brez sprožitve izjeme. Napačno postavljen klic preprosto izgine. Če med razvojem ovijete povratne vrednosti v trditve (assertions), lahko opazite odstopanja, dokler je koda še vidna. V nasprotnem primeru se manjkajoči obseg (scope) pokaže šele takrat, ko revizija dostopnosti preveri tabelo z dejanskim bralnikom zaslona. ID-ji elementov, posredovani prek funkcije BeginTagEx2, napajajo drevo ID-jev (ISO 32000-1 §14.7.4), kar sploh omogoča razrešitev povezave SetStructElemHeaders.

Družina istih atributov pokriva tudi preostale elemente, na katere se opira podporna tehnologija. SetStructElemListNumbering določa, kako so označeni elementi seznama, tako da bralnik zaslona napove položaj v seznamu, namesto da bi bral znake za oznake (bullets). SetStructElemBBox beleži omejitveni okvir (bounding box) slik in tabel, ki ga pogledi za prelamljanje vsebine uporabljajo za namestitev vsebine. SetStructElemActualText zagotavlja nadomestno besedilo za nize, katerih znaki se ne preslikajo v berljive črke (na primer začetna okrasna črka, sestavljena iz vektorske grafike). Vsak od njih sledi enakemu pravilu: poveže se z odprto oznako ali pa izgine.

Artefakti, jezik in diagnostična vrata pred shranjevanjem

Ponavljajoči se elementi strani, kot so tekoče glave, oznake za zlaganje, vodni znaki in barvna ozadja, sodijo znotraj oklepajev BeginArtifact in EndArtifact, tako da nikoli ne vstopijo v bralni tok. Jezik se deduje. Privzeti jezik dokumenta izhaja iz argumenta SetPDFUAMode, zapis v drugem jeziku pa ga prepiše za posamezen element prek funkcije BeginTagEx or SetStructElemLang. To omogoča, da je npr. francoski citat v angleškem priročniku pravilno izgovorjen.

Pred shranjevanjem funkcija GetPDFUADiagnostics izvede strukturne preglede knjižnice nad dokumentom v pomnilniku in vrne ugotovitve kot besedilo (prazni niz pomeni, da ni bilo najdenih napak). Kode neposredno označujejo klasične avtorske napake: FIGURE-NO-ALT za sliko brez nadomestnega besedila, HEADING-LEVEL-SKIP za naslov H3, ki sledi naslovu H1, ter ROLEMAP-UNMAPPED za tip po meri, ki ni bil nikoli registriran. Če to vključite v postopek gradnje (generiranje dokumentov in prekinitev koraka v primeru neprazne diagnostike), postanejo odstopanja pri dostopnosti napake pri prevajanju, namesto da bi jih kot ugotovitve revizije odkrili šele mesece pozneje. Končna ocena skladnosti sicer še vedno sodi v fazo preflighta shranjene datoteke, kar je opisano v članku preflight PDF/A in PDF/UA v Delphiju, saj se nekatere normalizacije izvedejo šele med serializacijo.

Navigacija po anotacijah ima lasten gumb. PDF/UA pričakuje, da premikanje s tipkovnico po obrazcih in povezavah sledi vrstnemu redu strukture. Funkcija SetTabOrderMode zapise vnos tab-order na ravni strani, ki ga bralniki upoštevajo, GetTabOrderMode pa je na voljo za revizijo prejetih datotek. To je vrsta zahteve, ki je nihče ne opazi, dokler uporabnik, ki uporablja le tipkovnico, ne prijavi napake, rešitev pa zahteva le een klic na dokument.

Strukturna drevesa ne preživijo vsakega združevanja

Označeni dokumenti ostanejo označeni le, če vsak naslednji korak obdelave ohrani drevo strukture. Ostra meja v PDFlibPas je družina funkcij za združevanje seznamov. Funkcija MergeFileListFast zamenja ohranjanje strukturnega drevesa za hitrost. To je pravilna izbira za pakete skeniranih slik in napačna za označena poročila, saj se izhodna datoteka odpre brez težav in izriše enako, vendar je tiho izgubila svojo plast dostopnosti. Uporabite privzeto funkcijo MergeFileList ali njeno strogo različico vedno, ko je kateri koli vhodni dokument označen, in vključite preverjanje IsTaggedPDF v končne trditve po sestavljanju, da preprečite pošiljanje dokumentov brez oznak. Cevovodi za sestavljanje velikih nizov dokumentov prinašajo več tovrstnih kompromisov, ki so raziskani v članku združevanje, razdeljevanje in neposreden dostop do velikih datotek PDF.

Verifikacijska zanka se zaključi zunaj knjižnice: odprite izhodno datoteko v programu Acrobat, preglejte ploščo z oznakami (tags) in z dejanskim bralnikom zaslona preberite vsaj en dokument iz vsake družine predlog. Diagnostika odkrije strukturne napake, le človeško uho pa lahko zazna vrstni red branja, ki je sicer tehnično pravilen, a v praksi povsem nerazumljiv. Ocene različic in celotna referenca API-ja za označevanje so na voljo na strani izdelka losLab PDF Library za Delphi.