Technical Article

Struktūrinių PDF medžių kūrimas „Delphi“ programoje su „PDFlibPas“

Prieinamas (angl. accessible) PDF remiasi viena struktūra, kurios matomas puslapis niekada nerodo: struktūros medžiu (angl. structure tree), apibrėžtu ISO 32000-1 §14.7. Tai yra loginė antraščių, pastraipų, lentelių ir paveikslėlių hierarchija, sluoksniuojama virš nupiešto turinio ir susiejama su standartiniais vaidmenimis per vaidmenų žemėlapį (angl. role map). Ekrano skaitytuvas (angl. screen reader) skaito šį medį, o ne tiesiog žymes puslapyje. Be jo sugeneruota sąskaita-faktūra, kuri vizualiai atrodo nepriekaištingai, yra semantiškai tuščia, nes turinio sraute įrašoma tik piešimo tvarka ir nieko daugiau. Galutinė suma gali būti perskaityta prieš atskiras eilutes, puslapio porštė gali įsiterpti į pastraipos vidurį, o prekių lentelė gali susilieti į vieną vientisą žodžių srautą. Išlaidos, skirtos tam išvengti, yra minimalios. Struktūros kūrimas piešimo metu užima vos kelias minutes kodo rašymo, o jos pridėjimas į jau paruoštus dokumentus virsta sudėtingu perdarymo projektu. „losLab PDF Library“ („PDFlibPas“) atveria šį medį „Delphi“ ir „C++Builder“ kūrėjams per kelis iškvietimus, kurie apgaubia kiekvieną piešimo operaciją jos loginiu vaidmeniu.

Kaip pažymėtas turinys susiejamas su struktūros medžiu

Kartu veikia du sluoksniai. Turinio sraute piešimo operacijos yra sugrupuojamos į pažymėto turinio sekas (angl. marked-content sequences), kurių kiekviena turi sveikąjį skaičių MCID. Dokumento kataloge struktūros medis susieja šiuos MCID su struktūrizuotų elementų hierarchija (H1, P, Table, Figure), turinčia papildomų atributų, tokių kaip alternatyvus tekstas ir kalba. Pasirinktiniai elementų tipai yra leidžiami, tačiau kiekvienas iš jų privalo būti susietas su standartiniu vaidmeniu per vaidmenų žemėlapį (ISO 32000-1 §14.8.4). Turinys, kuris neturi jokios prasminės reikšmės, pavyzdžiui, linijos, fonai ar pasikartojantys puslapio elementai, yra pažymimi kaip artefaktai, kad pagalbinė technologija juos praleistų ir neskaitytų vidury sakinio.

„PDFlibPas“ valdo abu šiuos sluoksnius naudodama vieną skliaustų porą. BeginTag atveria struktūros elementą ir pradeda pažymėto turinio seką, piešimo iškvietimai vykdomi jos viduje, o EndTag uždaro abu elementus. Visas sudėtingas žymėjimo valdymas (MCID, tėvinis medis ir puslapių nuorodos) atliekamas bibliotekos viduje, kur negalite padaryti klaidų.

Du dokumento lygio nustatymai apibrėžia darbą prieš atidarant bet kokią žymę. SetMarkInfo įrašo katalogo vėliavėlę, deklaruojančią, kad dokumentas yra struktūrinis, o IsTaggedPDF nuskaito šią reikšmę. Tai yra paprastas pirmasis patikrinimas sprendžiant, ar gaunamas failas turi kokią nors struktūrą, kurią verta išsaugoti. Kalbos nustatymas turi du prieigos taškus. SetDocumentLanguage nustato numatytąją dokumento kalbą, o SetPDFUAMode nustato ją kaip dalį pilno PDF/UA suderinamumo. Failas gali būti sėkmingai struktūrizuotas ir nedeklaruojant visiško PDF/UA atitikimo, o laipsniškas diegimas dažniausiai prasideda būtent nuo to.

Žymėjimas piešimo metu, o ne po jo

Teisingas dokumentų generavimo šablonas reikalauja žymėjimo skliaustus traktuoti kaip kiekvieno piešimo iškvietimo dalį, o ne kaip vėlesnį apdorojimo etapą:

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;

Trys iškvietimai šioje sekoje yra ypač svarbūs atitikties požiūriu. SetPDFUAMode įjungia PDF/UA režimą ir automatiškai pakeičia dokumento versiją į PDF 1.7, o tai gali sukelti konfliktų, jei versija yra griežtai nurodyta. Dokumentas, užfiksuotas ties PDF 1.4 versija naudojant LockSaveVersion, nebus išsaugotas ir grąžins klaidos kodą 602, kai bus aktyvuotas UA režimas. Šis konfliktas dažnai iškyla, kai archyvavimo profilius ir prieinamumo reikalavimus nustato skirtingos komandos. Funkcija SetInformation(1, ...) įrašo dokumento pavadinimą, kurį ISO 14289 reikalauja rodyti vietoj failo vardo. Jo trūkumas yra viena dažniausių PDF/UA klaidų. AddRoleMap privalomai užregistruoja pasirinktinį tipą ManualTitle kaip standartinį H1 vaidmenį. To nepadarius, žemiau aprašyta diagnostika užfiksuos nesusietą vaidmenį.

Antraščių lygiai reikalauja apgalvotos struktūros, o ne atsitiktinių pasirinkimų pagal puslapio dizainą. Ekrano skaitytuvų naudotojai pereina iš vieno skyriaus į kitą naudodami antraščių sparčiuosius klavišus. Todėl šablonas, kuriame po H1 eina H3 (nes tarpinis lygis pasirodė per didelis vizualiniame dizaine), tyliai sulaužo šią navigaciją, o vizualinė peržiūra to niekada nepastebės. Tai yra būtent tas defektas, kurį nustato diagnostinė klaida HEADING-LEVEL-SKIP. Vieną kartą vienoje vietoje susiekite kiekvieno šablono vizualinius stilius su griežta antraščių hierarchija, ir šios problemos išvengsite.

Lentelės, kuriose ekrano skaitytuvas gali orientuotis

Nupieštos tinklelio linijos ekrane nereiškia nieko. Ekrano skaitytuvai remiasi struktūriniais ryšiais: kurios langeliai yra antraštės, kokias sritis kiekviena antraštė valdo ir kaip duomenų langeliai susiejami su antraštėmis nestandartiniuose išdėstymuose. Struktūros elemento atributų iškvietimai atlieka visas tris užduotis:

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

Užsakymo taisyklė yra griežta ir vykdoma automatiškai. Kiekvienas SetStructElem* iškvietimas taikomas tai žymei, kuri yra atidaryta tuo momentu, tarp jos BeginTag ir EndTag. Jei jokia žymė neatidaryta arba atributas netaikomas dabartiniam elementui, funkcija tiesiog grąžina 0 nesukeldama klaidos. Neteisingoje vietoje atliktos iškvietimas tiesiog išnyksta be pėdsakų. Vykdant kūrimo procesą naudinga tikrinti grąžinamas reikšmes su assertions, kad iškart pastebėtumėte nukrypimus. Palikus šią problemą nespręstą, trūkstamas apibrėžimas išryškės tik atliekant prieinamumo auditą su realiu ekrano skaitytuvu. Elementų ID, perduodami per BeginTagEx2, užpildo ID medį (ISO 32000-1 §14.7.4), ir tai leidžia sėkmingai išspręsti SetStructElemHeaders susiejimą.

Ta pati atributų šeima apima ir kitus svarbius nustatymus pagalbinei technologijai. SetStructElemListNumbering nurodo, kaip žymimi sąrašo elementai, todėl ekrano skaitytuvas praneša apie elemento poziciją sąraše, o ne tiesiog skaito sąrašo ženklelius. SetStructElemBBox įrašo paveikslėlių ir lentelių ribojantįjį rėmelį (angl. bounding box), kurį naudoja adaptyvūs peržiūros režimai. SetStructElemActualText pateikia alternatyvų tekstą toms sritims, kurių simboliai neatitinka skaitomo teksto, pavyzdžiui, dekoratyvinei pirminei raidei, nupieštai iš vektorinių kreivių. Kiekvienas iš jų vadovaujasi ta pačia taisykle: yra susiejamas su atidaryta žyme arba tiesiog išnyksta.

Artefaktai, kalba ir prieš išsaugojimą atliekama diagnostika

Pasikartojantys puslapio elementai, tokie kaip bėgančios antraštės, lenkimo žymės, vandens ženklai ir fono atspalviai, privalo būti įterpiami tarp BeginArtifact ir EndArtifact skliaustų, kad nepatektų į skaitomo teksto srautą. Kalbos nustatymai yra paveldimi. Dokumento numatytoji kalba perduodama per SetPDFUAMode argumentą, o kitoje kalboje parašytas fragmentas gali būti pakeistas konkrečiam elementui per BeginTagEx arba SetStructElemLang. Tai leidžia taisyklingai ištarti prancūzišką citatą anglų kalba parašytame vadove.

Prieš išsaugant failą, funkcija GetPDFUADiagnostics paleidžia bibliotekos struktūrinius patikrinimus atmintyje esančiam dokumentui ir grąžina rezultatus kaip tekstą (tuščia eilutė reiškia, kad klaidų nerasta). Diagnostikos kodai tiesiogiai įvardija dažniausias kūrimo klaidas: FIGURE-NO-ALT rodo paveikslėlį be alternatyvaus teksto, HEADING-LEVEL-SKIP rodo po H1 einančią H3 antraštę, ROLEMAP-UNMAPPED indikuoja pasirinktinį tipą, kuris nebuvo užregistruotas. Integruokite tai į kūrimo procesą (sugeneruokite dokumentus, nutraukite žingsnį, jei diagnostika ne tuščia), ir prieinamumo nukrypimai taps kompiliavimo lygio klaidomis, o ne audito išvadomis po kelių mėnesių. Visapusiškas suderinamumo vertinimas vis tiek turi būti atliekamas jau išsaugotam failui, kaip aprašyta straipsnyje PDF/A ir PDF/UA patikra „Delphi“ programoje, nes kai kurios normalizacijos taikomos tik serializacijos metu.

Anotacijų navigacija taip pat turi savo valdiklį. PDF/UA reikalauja, kad formos laukų ir nuorodų perėjimas klaviatūra atitiktų struktūros tvarką. SetTabOrderMode įrašo puslapio lygio perėjimo tvarką, kurios laikosi peržiūros programos, o GetTabOrderMode naudojama gaunamų failų auditui. Tai yra reikalavimas, kurio niekas nepastebi, kol klaviatūra besinaudojantis vartotojas nepateikia pranešimo apie klaidą, o jo išsprendimas kainuoja tik vieną iškvietimą dokumentui.

Struktūros medžiai išlieka ne po kiekvieno sujungimo

Struktūriniai dokumentai išlaiko savo medį tik tada, kai kiekvienas tolesnis apdorojimo etapas jį išsaugo. Pavojinga funkcijų grupė „PDFlibPas“ bibliotekoje yra failų sujungimo sąrašai (angl. merge-list). Funkcija MergeFileListFast paaukoja struktūros medžio išsaugojimą vardan didesnio greičio. Tai tinkamas pasirinkimas skenuotų vaizdų paketams, tačiau visiškai netinkamas struktūriniams pranešimams, nes galutinis failas atsidaro gerai, atvaizduojamas identiškai, tačiau yra praradęs prieinamumo sluoksnį. Kai bent vienas įvesties failas yra struktūrinis, naudokite numatytąją funkciją MergeFileList arba griežtąjį variantą ir įtraukite IsTaggedPDF į patikras po sujungimo, kad suplotas paketas nebūtų išsiųstas nepastebėtai. Didelių dokumentų rinkinių sujungimo procesai turi daugiau panašių kompromisų, kurie nagrinėjami straipsnyje didelio PDF sujungimas, skaidymas ir tiesioginė prieiga.

Verifikavimo ciklas baigiasi už bibliotekos ribų: atidarykite rezultatą „Acrobat“ programoje, patikrinkite žymių skydelį ir perskaitykite bent vieną dokumentą iš kiekvieno šablono šeimos naudodami tikrą ekrano skaitytuvą. Diagnostika nustato struktūrines klaidas; tačiau tik žmogaus ausis gali pastebėti skaitymo tvarką, kuri yra techniškai teisinga, bet praktiškai paini. Vertinimo versijas ir pilną žymėjimo API aprašymą rasite „losLab PDF Library for Delphi“ produkto puslapyje losLab PDF Library for Delphi.