Techninis straipsnis

PDF/UA struktūros medžio validavimas Delphi aplinkoje

Jūsų preflight ataskaita failą paskelbia PDF/UA tvarkingu. veraPDF atidaro tą patį failą ir pagal 7.3 punktą pažymi Figure be alternatyvaus teksto. Abu įrankiai teisūs, ir tarpas tarp jų yra visa problema, kuri kyla tikrinant prieinamumą vien tik skenuojant baitus. Baitų lygio praėjimas patvirtina, kad failas sako esąs pažymėtas: jis randa /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part XMP pakete, dokumento pavadinimą ir kalbą. Tai formato žymekliai, ir jie būtini. Tačiau jie nieko nepasako apie tai, ar tikrasis paveikslas ketvirtame puslapyje turi aprašą, kurį ekrano skaitytuvas gali perskaityti balsu. Tas atsakymas gyvena žymų medyje, ir norint jį gauti reikia medį pereiti

PDFium Component yra vietinė VCL PDF biblioteka, skirta Delphi ir C++Builder, ir jos ValidatePdfUa atlieka abu praėjimus. Baitų lygio praėjimas tvarko formato žymeklius. Ant jo viršaus sėdi struktūros medžio praėjimas, kuris įkelia gyvą žymėtą medį, pereina per kiekvieną elementą ir tikrina mažą rinkinį aukšto pasitikėjimo turinio taisyklių, kai trūkstamas atributas reiškia tikrą prieinamumo defektą, o ne stilistinį pasirinkimą. Šis straipsnis yra apie tą antrąjį praėjimą: ką jis tikrina, kodėl taisyklių logika yra gryna funkcija be jokio DLL po savimi ir kur ji sąmoningai sustoja

Kodėl baitų skenavimas negali pamatyti trūkstamo Alt

ISO 14289-1 (PDF/UA-1) yra reikalavimų sluoksnis ant ISO 32000. Kai kurie iš tų reikalavimų yra struktūriniai ir matomi žaliame faile: katalogas turi deklaruoti struktūros medį, viewer preferences turi nustatyti DisplayDocTitle, šriftai turi būti įterpti. Tokenų skeneris, kuris pašalina stream body ir vardinius tokenus tikrina su ribų skyrikliais, visa tai gali patvirtinti, ir PDFium ValidatePdfUaCompliance būtent tai ir daro tokiems punktams kaip 7.1, 7.18 ir 7.21

Tačiau teiginys „kiekviena Figure turi alternatyvų tekstą“ nėra failo sintaksės savybė. Tai yra loginės struktūros savybė - pažymėtų elementų medžio, kuris susieja turinį su reikšme. Figure elementas gali turėti Alt įrašą savo struktūros elemento žodyne, gali tiekti tekstą per /ActualText span arba gauti jį iš role-mapped individualaus tipo. Patikimai to neaptiksite vien ieškodami /Alt baitų sraute, nes ši eilutė pasirodo ir nesusijusiuose kontekstuose, gali būti suspausta object stream viduje ir nieko nepasako apie tai, kuriam struktūros elementui ji priklauso. Sąžiningas būdas atsakyti į klausimą yra klausti paties dokumento struktūros medžio, elementas po elemento, to paties paviršiaus, kurį vertina veraPDF ir PAC. Tokia ir yra riba, ant kurios pastatyti PDFium Tier-1 patikrinimai: baitų skenavimas formatui, medžio perėjimas turiniui

Gyvo žymų medžio nuskaitymas

Žaliava čia yra TPdf.GetStructureElements (taip pat pateikta kaip savybė StructureElements), kuri grąžina TPdfStructureElements - plokščią TPdfStructureElement įrašų masyvą dokumento tvarka. Kiekvienas įrašas yra vieno struktūros elemento projekcija per PDFium prieigos funkcijas ir laiko būtent tuos laukus, kurių reikia prieinamumo taisyklėms:

type
  TPdfStructureElement = record
    Level: Integer;            // depth in the tag tree
    ParentIndex: Integer;      // index of parent element, or -1
    TypeName: WString;         // standard /S name: Figure, Formula, Note...
    Title: WString;            // /T
    AlternateText: WString;    // /Alt   (FPDF_StructElement_GetAltText)
    ActualText: WString;       // /ActualText
    Expansion: WString;        // /E
    ID: WString;               // /ID    (FPDF_StructElement_GetID)
    Language: WString;         // /Lang
    MarkedContentIDs: TPdfIntegerArray;
    // ... child bookkeeping fields
  end;

Laukas TypeName yra tas, ant kurio validatorius remiasi labiausiai. Jis ateina iš FPDF_StructElement_GetType, kuris po to, kai PDFium išsprendžia role map, grąžina standartinį elemento struktūros tipą - jo /S vardą. AlternateText ateina iš FPDF_StructElement_GetAltText, ActualTextFPDF_StructElement_GetActualText, o ID - iš FPDF_StructElement_GetID. Kadangi masyvas plokščias ir sutvarkytas, validatorius gali samprotauti apie visą dokumentą vienu kartu, o ne rekursiškai - tai svarbu vienai taisyklei, kuri yra globali, o ne per-elementinė

Tikrintuvas yra gryna funkcija, ir taip padaryta sąmoningai

Taisyklių logika negyvena to metodo viduje, kuris kalbasi su DLL. Ji pateikta kaip atskira, vieša, gryna funkcija:

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

Ji priima plokščią elementų masyvą ir grąžina problemų aibę. Ji nekviečia jokios PDFium funkcijos, neatidaro jokio dokumento ir neliečia jokios globalios būsenos. Šis atskyrimas sąmoningas ir atsiperka dukart. Pirma, testuojamumu: vieneto teste galite susikurti sintetinį TPdfStructureElements masyvą - Figure be Alt, Formula, kurios vienintelis prieinamas tekstas yra ActualText, dvi Notes su tuo pačiu ID - ir tikrinti rezultatų aibę net tada, kai pdfium.dll visiškai nėra. Taisyklių logika tikrinama neprisijungus; DLL perėjimas tikrinamas atskiru gyvo dokumento smoke test, kuris praleidžiamas, kai bibliotekos nėra

Antra, atsakomybės aiškumu. TPdf.ValidatePdfUa valdo nešvariąją dalį - kiekvieno puslapio įkėlimą, elementų ištraukimą ir jų kaupimą - ir tada perduoda švarų masyvą grynam tikrintuvui. „Gauti duomenis“ (DLL, šalutiniai poveikiai, gyvavimo trukmė) ir „vertinti taisykles“ (gryna, determinuota logika) niekada nesusipina. Kai taisyklę reikia keisti, keičiate funkciją, kurioje nėra jokio I/O

Ką tiksliai tikrina šios trys taisyklės

Struktūros medžio praėjimas pakelia tris problemų reikšmes, pridėtas į TPdfUaValidationIssues pabaigą tam, kad enum išliktų ABI-stabilus esamiems kvietėjams: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt ir pvuaiNoteMissingId. Pats kūnas pakankamai mažas, kad jį būtų galima iki galo suprasti:

for I := 0 to High(Elements) do
begin
  T := string(Elements[I].TypeName);
  if T = 'Figure' then
  begin
    // §7.3 — a Figure needs an alternate representation:
    // an Alt entry OR ActualText. Flag only when BOTH are empty.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFigureMissingAlt);
  end
  else if T = 'Formula' then
  begin
    // §7.7 — same rule as Figure: Alt OR ActualText.
    if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
      Include(Result, pvuaiFormulaMissingAlt);
  end
  else if T = 'Note' then
  begin
    // §7.9 — every Note must have a unique ID.
    NoteId := string(Elements[I].ID);
    if NoteId = '' then
      Include(Result, pvuaiNoteMissingId)
    else
      for J := 0 to I - 1 do
        if (string(Elements[J].TypeName) = 'Note') and
           (string(Elements[J].ID) = NoteId) then
        begin
          Include(Result, pvuaiNoteMissingId);
          Break;
        end;
  end;
end;

7.3 punktas reguliuoja figuras: Figure elementas turi pateikti tekstinę alternatyvą. Ankstyva šios taisyklės versija žiūrėjo tik į Alt įrašą, todėl buvo griežtesnė nei etaloniniai validatoriai. PDF/UA priima figūrą, kurios prieinamas tekstas tiekiamas per ActualText - pakaitinis tekstas yra galiojanti alternatyvi reprezentacija - todėl taisyklė Figure pažymi tik tada, kai ir Alt, ir ActualText yra tušti. 7.7 punktas apima formules, ir po tos pačios korekcijos naudoja identišką Alt-or-ActualText testą; atitikties korpuso pavyzdys, kuriame Formula prieinamas tekstas buvo pateiktas vien per ActualText, anksčiau klaidingai atmesdavo, kol Formula šaka nebuvo sulyginta su Figure šaka

7.9 punktas yra kitokios rūšies. Note privalo turėti /ID, ir tas ID turi būti unikalus visame dokumente. Trūkstamas ID yra per-elementinė nesėkmė. Dublikuotas ID yra santykis tarp dviejų elementų, todėl plokščias masyvas čia ir svarbus: kiekvienai Note tikrintuvas atsisuka atgal per jau matytus elementus ir pažymi susidūrimą su bet kuria ankstesne Note, nešančia tą patį ID. Kaina čia akivaizdi O(n²) pagal Notes kiekį, bet bet kuriam realiam dokumentui ji nereikšminga, o funkcija dėl to lieka viena skaitoma ciklo atkarpa be jokio papildomo indekso, kurį dar reikėtų sinchronizuoti

Kaupimas per puslapius tam, kad unikalumas būtų globalus

PDFium struktūros elementus pateikia pagal puslapį, o ne pagal dokumentą, todėl orkestracija ValidatePdfUa viduje pirmiausia turi juos sukaupti. Ji pereina kiekvieną puslapį su FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, nepriklausomai nuo to, kuris puslapis komponente šiuo metu atidarytas, ir kiekvieno puslapio elementus prideda į vieną masyvą. Tik tada kviečiamas grynasis tikrintuvas:

// inside TPdf.ValidatePdfUa, after the byte-level pass
if (FDocument <> nil) and
   (not (pvuaiMissingStructTreeRoot in Result.Issues)) then
begin
  AllElems := nil;
  PageTotal := FPDF_GetPageCount(FDocument);
  for I := 0 to PageTotal - 1 do
  begin
    Page := FPDF_LoadPage(FDocument, I);
    if Page = nil then Continue;
    try
      PageElems := GetStructureElementsForPage(Page);
    finally
      FPDF_ClosePage(Page);
    end;
    // append PageElems into AllElems ...
  end;
  Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;

Būtent kaupimas ir padaro 7.9 unikalumo patikrą teisingą. Dvi Notes skirtinguose puslapiuose gali dalytis tuo pačiu ID; jei tikrintumėte puslapis po puslapio, niekada nepamatytumėte susidūrimo, nes kiekvieno puslapio elementų rinkinys atrodytų viduje nuoseklus. Vieno dokumento masto masyvo sudarymas yra vienintelis būdas dubliavimą padaryti matomą. Priekinis saugiklis čia taip pat vertas dėmesio: medžio perėjimas vykdomas tik tada, kai baitų lygio praėjimas nepranešė apie pvuaiMissingStructTreeRoot. Nežymėtas dokumentas neturi jokio medžio, kurį būtų galima eiti, ir jau būna pažymėtas dėl trūkstamos struktūros šaknies, todėl per-puslapio įkėlimai išvis praleidžiami. Gilusis praėjimas nieko nekainuoja dokumentuose, kuriems vis tiek negalėtų padėti

Pagal sumanymą konservatyvus: tyliai praleisti, bet niekada nekelti melagingo aliarmo

Svarbiausia šio validatoriaus savybė yra tai, ko jis atsisako daryti. Jis derina tik standartinius /S tipo vardus, kuriuos FPDF_StructElement_GetType grąžina tiesiogiai - Figure, Formula, Note. Dokumentas, kuris apibrėžia nuosavą tipą ir role-map būdu susieja jį su Figure, priklausomai nuo to, kaip PDFium išsprendžia tipą, gali pranešti savo nuosavą vardą. Kai taip atsitinka, tikrintuvas jo neatpažįsta ir tyli. Tai yra klaidingas neigiamas rezultatas, ir būtent taip ir sumanyta. Dizaino taisyklė čia tokia: geriau nepranešti per mažai nei kada nors sugeneruoti klaidingą teigiamą radinį, nes preflight įrankis, kuris melagingai loja ant atitinkančių failų, išmoko naudotojus jį ignoruoti, o ignoruojamas validatorius yra blogiau nei jokio. Dekoratyvūs paveikslai gyvena artifact sraute, o ne struktūros medyje, todėl jie apskritai neiškyla kaip Figures; jūs negausite „trūksta Alt“ skundo dėl foninės linijos, kuri teisingai pažymėta kaip artifact

Dėl to ir taikymo sritis sąmoningai laikoma ties trimis taisyklėmis. Antraščių lygių lizdavimas (7.4 punktas), lentelių antraščių sritis (7.5) ir role-map ciklų aptikimas (7.1) yra visiškai teisėti PDF/UA reikalavimai, tačiau norint juos tikrinti gerai, reikia tikros grafų ir atributų analizės, o bandant tai daryti naiviai gaunamos būtent tos klaidingos teigiamos išvados, kurių šis dizainas vengia - PDF/UA leidžia antraščių modelius, tokius kaip H1, H2, H3, H3, kuriuos paprasta taisyklė „turi griežtai didėti“ neteisingai atmestų. Tokios patikros paliekamos specializuotiems atitikties įrankiams. Tier-1 rinkinys yra tik tas poaibis, kuriame trūkstamas atributas yra nedviprasmis

Riba, įvardyta tiesiai

Dvi ribos vertos žinojimo prieš įvedant šį įrankį į leidimo vartus. Pirma, tikrintuvas yra tiek geras, kiek gerai PDFium iš struktūros elemento gali išskaityti informaciją. Nedidelė dalis atitikties korpuso failų, kuriuos etaloniniai validatoriai priima, naudoja alternatyvaus teksto mechanizmą, kurio PDFium nepateikia, todėl FPDF_StructElement_GetAltText grąžina tuščią reikšmę, nors failas iš tikrųjų atitinka standartą. Tada grynasis tikrintuvas „teisingai“ pažymi trūkstamą Alt, nors duomenys jam buvo neišsamūs - tai klaidingas teigiamas radinys, kylantis iš DLL prieigos aprėpties, o ne iš taisyklės logikos. Atlaisvinti taisyklę taip, kad ji šiuos atvejus sugertų, reikštų ją apakinti ir tikroms klaidoms, kurias ji turi pagauti, todėl tokie atvejai dokumentuojami kaip žinomas PDFium apribojimas, o ne užtepami kosmetika

Antra, tai yra preflight, o ne sertifikacija. Tier-1 pagauna aukšto pasitikėjimo turinio klaidas, kurių baitų skenavimas struktūriškai negali pamatyti, ir tai daro nekeldamas melagingų aliarmų, tačiau pilna PDF/UA atitiktis, įskaitant antraščių semantiką, lentelių struktūrą ir skaitymo eilės teisingumą, vis tiek priklauso pilnam validatoriui ir galų gale žmogaus peržiūrai. Naudokite ValidatePdfUa tam, kad savo vamzdyne greitai ir pigiai atmestumėte akivaizdžius defektus, o galutinį žodį palikite veraPDF arba PAC. Tas pats struktūros medžio perėjimas yra pagrindas kuriant prieinamą PDF skaitytuvą Delphi aplinkoje, kur žymų medis valdo skaitymo eilę ir tariamą tekstą, ir jis gražiai papildo metaduomenų lygio darbą, aptartą PDF anotacijų peržiūroje iš Delphi

Čia parodyti struktūros medžio API ir validatorius ValidatePdfUa tiekiami kartu su PDFium Component, skirtu Delphi ir C++Builder (VCL) bei Lazarus/FPC (LCL). Produkto puslapyje pateikiamas visas API nuorodynas, įskaitant pilną TPdfStructureElement įrašo išdėstymą ir problemų enum, ant kurių šios patikros pastatytos