Tehnični članak

Validacija strukturnega drevesa PDF/UA v Delphi s PDFium

Vaš preflight prijavi datoteko kot čisto po PDF/UA. veraPDF odpre isto datoteko in označi Figure brez nadomestnega besedila po členu 7.3. Obe orodji imata prav, razkorak med njima pa je celoten problem preverjanja dostopnosti s pregledom bajtov. Prehod na ravni bajtov potrdi, da datoteka pravi, da je označena: najde /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part v paketu XMP, naslov dokumenta, jezik. To so označevalci formata in so nujni. Ne povedo pa nič o tem, ali ima dejanska slika na četrti strani opis, ki ga lahko bralnik zaslona prebere na glas. Ta odgovor živi v drevesu oznak, in če ga želite dobiti, morate skozi drevo

PDFium Component je izvorna knjižnica VCL za PDF v Delphi in C++Builder, njen ValidatePdfUa pa opravi oba prehoda. Prehod na ravni bajtov obdela označevalce formata. Nad njim sedi prehod po strukturnem drevesu, ki naloži živo označeno drevo, prehodi vsak element in preveri majhen nabor pravil o vsebini z visoko stopnjo zaupanja, kjer manjkajoč atribut pomeni resnično napako dostopnosti, ne pa slogovne preference. Ta članek govori o tem drugem prehodu: kaj preverja, zakaj je logika pravil čista funkcija brez DLL pod sabo in kje se namenoma ustavi

Zakaj pregled bajtov ne more videti manjkajočega Alt

ISO 14289-1 (PDF/UA-1) je plast zahtev nad ISO 32000. Nekatere od teh zahtev so strukturne in vidne v surovi datoteki: katalog mora deklarirati strukturno drevo, nastavitve pregledovalnika morajo nastaviti DisplayDocTitle, pisave morajo biti vdelane. Pregledovalnik žetonov, ki odstrani telesa tokov in z mejami ločil primerja imenske žetone, lahko vse to preveri, in PDFiumov ValidatePdfUaCompliance počne natanko to za člene, kot so 7.1, 7.18 in 7.21

Toda "vsak Figure ima nadomestno besedilo" ni lastnost sintakse datoteke. To je lastnost logične strukture - drevesa označenih elementov, ki vsebino poveže s pomenom. Vnos Alt za Figure lahko sedi v slovarju strukturnega elementa, je podan prek razpona /ActualText ali pa prihaja iz prilagojenega tipa, preslikanega z vlogo. Tega ne morete zanesljivo najti z iskanjem /Alt v toku bajtov, ker se ta niz pojavlja v nepovezanih kontekstih, je lahko stisnjen znotraj object stream in ne pove nič o tem, kateremu strukturnemu elementu pripada. Pošten način odgovora na to vprašanje je, da dokumentovo lastno strukturno drevo povprašate element za elementom, na isti površini, ki jo ocenjujeta veraPDF in PAC. To je črta, okoli katere so zgrajena preverjanja Tier-1 v PDFium: pregled bajtov za format, prehod po drevesu za vsebino

Branje živega drevesa oznak

Surovina je TPdf.GetStructureElements (izpostavljeno tudi kot lastnost StructureElements), ki vrne TPdfStructureElements - plosko polje zapisov TPdfStructureElement v vrstnem redu dokumenta. Vsak zapis je projekcija enega strukturnega elementa skozi accessor funkcije PDFium, z ravno tistimi polji, ki jih pravila dostopnosti dejansko potrebujejo:

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;

Polje TypeName je tisto, okoli katerega se validator vrti. Prihaja iz FPDF_StructElement_GetType, ki vrne standardni tip strukture elementa - njegovo ime /S - potem ko PDFium razreši preslikavo vlog. AlternateText prihaja iz FPDF_StructElement_GetAltText, ActualText iz FPDF_StructElement_GetActualText in ID iz FPDF_StructElement_GetID. Ker je polje plosko in urejeno, lahko validator sklepa o celotnem dokumentu naenkrat namesto z rekurzijo - kar je pomembno za eno pravilo, ki je globalno in ne per-element

Preverjalnik je čista funkcija, in to namenoma

Logika pravil ne živi znotraj metode, ki govori z DLL. Gre za samostojno, javno, čisto funkcijo:

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

Vzame plosko polje elementov in vrne množico težav. Ne kliče nobene funkcije PDFium, ne odpre nobenega dokumenta, ne dotakne se nobenega globalnega stanja. Ta ločitev je namerna in se dvakrat obrestuje. Prvič, preizkusljivost: v enotskem testu lahko zgradite sintetično polje TPdfStructureElements - Figure brez Alt, Formula, katere edino dostopno besedilo je v ActualText, dva Note z istim ID - in preverite množico rezultatov, ne da bi bil pdfium.dll sploh prisoten. Logika pravil je preverjena brez povezave; prehod skozi DLL je ločeno preverjen z dimnim testom na živem dokumentu, ki se preskoči, kadar knjižnice ni

Drugič, jasnost odgovornosti. TPdf.ValidatePdfUa je lastnik neurejenega dela - nalaganja vsake strani, vlečenja njenih elementov, njihovega kopičenja - nato pa čisto polje preda čistemu preverjalniku. "Pridobi podatke" (DLL, stranski učinki, življenjska doba) in "presodi pravila" (čisto, deterministično) se nikoli ne zapleteta. Ko je treba pravilo spremeniti, spremenite funkcijo, ki v sebi nima nobenega I/O

Kaj tri pravila dejansko preverjajo

Prehod po strukturnem drevesu sproži tri vrednosti težav, pripete na konec TPdfUaValidationIssues, da enumeracija za obstoječe klicatelje ostane ABI-stabilna: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt in pvuaiNoteMissingId. Telo je dovolj majhno, da ga lahko v celoti razumete:

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;

Člen 7.3 ureja slike: element Figure mora zagotoviti besedilno alternativo. Zgodnja različica tega preverjanja je gledala samo vnos Alt, zaradi česar je bila strožja od referenčnih validatorjev. PDF/UA sprejme sliko, katere dostopno besedilo je podano prek ActualText - nadomestno besedilo je veljavna alternativna predstavitev - zato pravilo označi Figure samo takrat, ko sta oba Alt in ActualText prazna. Člen 7.7 pokriva formule, po istem popravku pa uporablja enak preizkus Alt-ali-ActualText; vzorec iz korpusa skladnosti, ki je Formula dal dostopno besedilo samo skozi ActualText, je bil lažno zavrnjen, dokler veja Formula ni bila poravnana z vejo Figure

Člen 7.9 je drugačne vrste. Note mora imeti /ID, ta ID pa mora biti enoličen v celotnem dokumentu. Manjkajoč ID je napaka per-element. Podvojen ID pa je razmerje med dvema elementoma, zato je plosko polje pomembno: za vsak Note preverjalnik pregleda nazaj po že videnih elementih in označi trk s katerimkoli prejšnjim Note z istim ID. Cena je očiten O(n²) glede na število Note, kar je za vsak resničen dokument nepomembno in funkcijo ohranja v enem berljivem zanki brez pomožnega indeksa, ki bi ga bilo treba usklajevati

Kopičenje čez strani, da je enoličnost globalna

PDFium izpostavi strukturne elemente po straneh, ne po dokumentih, zato mora orkestracija v ValidatePdfUa pred zagonom pravil najprej vse zbrati. Skozi vsako stran gre z FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, neodvisno od tega, katero stran ima komponenta trenutno odprto, in elemente vsake strani pripne v eno samo polje. Šele potem pokliče čisti preverjalnik:

// 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;

Prav to kopičenje naredi preverjanje enoličnosti iz 7.9 pravilno. Dva Note na različnih straneh si lahko delita isti ID; če bi validirali stran po stran, trka nikoli ne bi videli, ker je nabor elementov vsake strani videti notranje dosleden. Zgraditi eno samo polje za cel dokument je edini način, da podvojitev postane vidna. Vredno je opaziti tudi varovalko na začetku: prehod po drevesu se zažene samo, kadar prehod na ravni bajtov ni prijavil pvuaiMissingStructTreeRoot. Neoznačen dokument nima drevesa, po katerem bi se dalo hoditi, in je že označen zaradi manjkajočega korena strukture, zato se nalaganje po straneh v celoti preskoči. Globoki prehod ne stane nič na dokumentih, ki od njega ne morejo imeti koristi

Namenoma konservativen: tiho zgreši, nikoli ne tuli lažno

Najpomembnejša lastnost tega validatorja je to, česar noče početi. Ujema se samo s standardnimi imeni tipov /S, ki jih FPDF_StructElement_GetType vrne neposredno - Figure, Formula, Note. Dokument, ki definira prilagojen tip in ga z vlogo preslika na Figure, bo - odvisno od tega, kako PDFium razreši tip - prijavil svoje lastno ime. Ko se to zgodi, ga preverjalnik ne prepozna in ostane tiho. To je lažno negativni rezultat in to je namenjeno vedenje. Pravilo zasnove je poročati premalo, namesto da bi kdaj ustvaril lažni pozitivni rezultat, ker preflight orodje, ki tuli pri skladnih datotekah, svoje uporabnike nauči, da ga ignorirajo - ignoriran validator pa je slabši kot noben. Dekorativne slike živijo v toku artifact, ne v strukturnem drevesu, zato se sploh nikoli ne pojavijo kot Figure; ne boste dobili pritožbe o "manjkajočem Alt" za ozadno črto, ki je pravilno označena kot artifact

Tudi zato je obseg omejen na tri pravila. Gnezdenje ravni naslovov (člen 7.4), obseg glav tabel (7.5) in zaznavanje ciklov preslikave vlog (7.1) so vse legitimne zahteve PDF/UA, vendar njihovo dobro preverjanje zahteva pravo analizo grafa in atributov, naivno preverjanje pa ustvari natanko tiste lažne pozitivne rezultate, ki jih zasnova prepoveduje - PDF/UA na primer dovoljuje vzorce naslovov, kot so H1, H2, H3, H3, ki bi jih preprosto pravilo "mora strogo naraščati" napačno zavrnilo. Ta preverjanja so prepuščena namenskim orodjem za skladnost. Nabor Tier-1 je podmnožica, kjer je manjkajoč atribut nedvoumen

Meja, povedana naravnost

Preden to povežete v izdajno zaporo, je vredno poznati dve meji. Prvič, preverjalnik je dober samo toliko, kolikor dobro PDFium prebere podatke iz strukturnega elementa. Peščica datotek iz korpusa skladnosti, ki jih referenčni validatorji sprejmejo, uporablja mehanizem nadomestnega besedila, ki ga PDFium ne izpostavi, zato FPDF_StructElement_GetAltText vrne prazno, čeprav je datoteka v resnici skladna. Čisti preverjalnik nato "pravilno" označi manjkajoč Alt na nepopolnih podatkih - lažni pozitivni rezultat, ki izvira iz pokritosti accessorjev DLL, ne iz logike pravil. Če bi pravilo razrahljali tako, da bi te primere pogoltnilo, bi ga hkrati oslepili za resnične napake, ki jih mora ujeti, zato so dokumentirani kot znana omejitev PDFium, namesto da bi jih prekrili

Drugič, to je preflight, ne certifikacija. Tier-1 ujame napake vsebine z visoko stopnjo zaupanja, ki jih pregled bajtov strukturno ne more, in to stori brez lažnih alarmov - toda popolna skladnost PDF/UA, vključno s semantiko naslovov, strukturo tabel in pravilnostjo vrstnega reda branja, še vedno pripada popolnemu validatorju in nazadnje človeškemu pregledovalcu. Uporabite ValidatePdfUa za hitro in poceni zavračanje očitnih napak v lastnem cevovodu, nato pa prepustite zadnjo besedo orodju veraPDF ali PAC. Isti prehod po strukturnem drevesu podpira tudi gradnjo dostopnega bralnika PDF v Delphi, kjer drevo oznak poganja vrstni red branja in govorjeno besedilo, ter dopolnjuje delo na ravni metapodatkov pri pregledovanju anotacij PDF iz Delphi

API-ji za strukturno drevo in validator ValidatePdfUa, prikazan tukaj, so del PDFium Component za Delphi in C++Builder (VCL) ter Lazarus/FPC (LCL). Stran izdelka povezuje celoten API reference, vključno s popolno postavitvijo zapisa TPdfStructureElement in enumeracijo težav za temi preverjanji