Tehnički članak

Validacija stabla PDF/UA strukture u Delphiju uz PDFium

Vaš preflight prijavi fajl kao čist za PDF/UA. veraPDF otvori isti fajl i prijavi Figure bez alternativnog teksta pod klauzulom 7.3. Oba alata su u pravu, a jaz između njih je ceo problem sa proverom pristupačnosti skeniranjem bajtova. Provera na nivou bajta potvrđuje da fajl tvrdi da je tagovan: pronalazi /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part u XMP paketu, naslov dokumenta, jezik. To su format markeri, i oni su neophodni. Ne govore vam ništa o tome da li stvarna figura na strani četiri nosi opis koji čitač ekrana može da izgovori naglas. Taj odgovor živi u stablu tagova, a da biste ga dobili morate da prođete kroz stablo

PDFium Component je nativna VCL PDF biblioteka za Delphi i C++Builder, a njen ValidatePdfUa radi oba prolaza. Prolaz na nivou bajta obrađuje format markere. Iznad njega stoji prolaz kroz stablo strukture koji učitava živo tagovano stablo, prolazi kroz svaki element i proverava mali skup pravila sadržaja sa visokom pouzdanošću gde nedostajući atribut znači stvarni problem pristupačnosti, a ne stilsku preferencu. Ovaj članak je o tom drugom prolazu: šta proverava, zašto je logika pravila čista funkcija bez DLL-a ispod nje i gde namerno staje

Zašto bajt skener ne može da vidi nedostajući Alt

ISO 14289-1 (PDF/UA-1) je sloj zahteva iznad ISO 32000. Neki od tih zahteva su strukturni i vidljivi u sirovom fajlu: katalog mora da objavi stablo strukture, viewer preferences mora da postavi DisplayDocTitle, fontovi moraju biti ugrađeni. Skener tokena koji skida tela tokova i traži name tokene sa granicama delimiter-a može da proveri sve to, a PDFium-ov ValidatePdfUaCompliance radi upravo to za klauzule poput 7.1, 7.18 i 7.21

Ali "svaka Figure ima alternativni tekst" nije osobina sintakse fajla. To je osobina logičke strukture - stabla tagovanih elemenata koje mapira sadržaj na značenje. Figure Alt zapis može da stoji u rečniku strukturalnog elementa, može da stigne kroz /ActualText span, ili da dođe iz role-mapped prilagođenog tipa. Ne možete pouzdano da ga pronađete grepom kroz /Alt u toku bajtova, jer se taj string pojavljuje i u nepovezanim kontekstima, može biti komprimovan unutar object stream-a, i ne govori vam ništa o kojem strukturalnom elementu pripada. Pošten način da se odgovori na pitanje jeste da se dokumentovom sopstvenom stablu strukture pristupi element po element, isto onako kako to rade veraPDF i PAC. To je linija oko koje su izgrađene PDFium-ove Tier-1 provere: skeniranje bajtova za format, obilazak stabla za sadržaj

Čitanje živog taga stabla

Sirovina je TPdf.GetStructureElements (takođe izloženo kao StructureElements svojstvo), koje vraća TPdfStructureElements - ravan niz od TPdfStructureElement zapisa u redosledu dokumenta. Svaki zapis je projekcija jednog strukturnog elementa kroz PDFium-ove pristupne funkcije, sa poljima koja su zaista potrebna pravilima pristupačnosti:

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 ono na koje se validator oslanja. Ono dolazi iz FPDF_StructElement_GetType, koje vraća standardni tip strukture elementa - njegovo /S ime - nakon što je PDFium razrešio role map. AlternateText dolazi iz FPDF_StructElement_GetAltText, ActualText iz FPDF_StructElement_GetActualText, a ID iz FPDF_StructElement_GetID. Pošto je niz ravan i uređen, validator može da rezonuje o celom dokumentu odjednom umesto rekurzivno - što je važno za jedno pravilo koje je globalno, a ne po elementu

Proverivač je čista funkcija, i to namerno

Logika pravila ne živi unutar metode koja razgovara sa DLL-om. To je samostalna, javna, čista funkcija:

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

Uzima ravan niz elemenata i vraća skup problema. Ne poziva nijednu PDFium funkciju, ne otvara nijedan dokument, ne dira globalno stanje. To razdvajanje je namerno, i isplati se dvaput. Prvo, testabilnost: možete da napravite sintetički TPdfStructureElements niz u unit testu - Figure bez Alt, Formula čiji je jedini pristupačni tekst u ActualText, dve Note koje dele ID - i da na rezultat skupa proverite bez pdfium.dll prisustva. Logika pravila se proverava offline; DLL obilazak se proverava posebno živim dokumentom koji se preskače kada biblioteka nedostaje

Drugo, jasnoća odgovornosti. TPdf.ValidatePdfUa nosi prljavi deo - učitavanje svake strane, izvlačenje njenih elemenata, sabiranje - a zatim predaje čist niz čistom proverivaču. "Uzmi podatke" (DLL, sporedni efekti, životni vek) i "sudi po pravilima" (čisto, deterministički) nikada se ne pomešaju. Kada pravilo treba da se menja, menjate funkciju koja u sebi nema I/O

Šta zapravo proveravaju tri pravila

Prolaz kroz stablo strukture podiže tri vrednosti problema, dodate na kraj TPdfUaValidationIssues tako da enum ostane ABI-stabilan za postojeće pozivaoce: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, i pvuaiNoteMissingId. Telo je dovoljno malo da se može potpuno razumeti:

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;

Klauzula 7.3 se odnosi na figure: Figure element mora da obezbedi tekstualnu alternativu. Rana verzija ove provere gledala je samo Alt zapis, što ju je činilo strožom od referentnih validatora. PDF/UA prihvata figuru čiji je pristupačni tekst dat kroz ActualText umesto toga - replacement text je važeća alternativna reprezentacija - pa pravilo prijavljuje Figure samo kada oba Alt i ActualText prazna. Klauzula 7.7 pokriva formule, a nakon iste korekcije koristi identičan Alt-ili-ActualText test; uzorak iz conformance korpusa koji je jednoj Formuli davao pristupačni tekst samo kroz ActualText bio je lažno odbijen dok grana za Formulu nije poravnata sa granom za Figure

Klauzula 7.9 je drugačija po prirodi. Note mora da ima /ID ID, i taj ID mora biti jedinstven kroz ceo dokument. Nedostajući ID je neuspeh po elementu. duplirani ID je odnos između dva elementa, zbog čega je ravan niz važan: za svaku Note, proverivač se vraća unazad preko elemenata koje je već video i prijavljuje sudar sa svakom ranijom Note koja nosi isti ID. Cena je očigledno O(n²) po broju Note elemenata, što je nevažno za svaki realan dokument i zadržava funkciju kao jednu čitljivu petlju bez pomoćnog indeksa koji bi morao da se usklađuje

Sabiranje preko strana da bi jedinstvenost bila globalna

PDFium izlaže elemente strukture po strani, a ne po dokumentu, pa orkestracija u ValidatePdfUa mora da ih prikupi pre nego što pravila krenu. Ona obilazi svaku stranu sa FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, nezavisno od toga koju stranu komponenta trenutno ima otvorenu, i dodaje elemente svake strane u jedan niz. Tek tada poziva čisti proverivač:

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

Sabiranje je ono što čini proveru jedinstvenosti iz 7.9 ispravnom. Dve Note na različitim stranama mogu da dele ID; ako biste proveravali stranu po stranu, nikada ne biste videli sudar, jer skup elemenata svake strane izgleda interno dosledno. Pravljenje jednog dokument-wide niza jedini je način da se duplikat pojavi. Vredi primetiti i prednji čuvar: obilazak stabla se pokreće samo kada je prolaz na nivou bajta uradio ne prijavio pvuaiMissingStructTreeRoot. Neoznačen dokument nema stablo koje bi se obilazilo i već je prijavljen zbog nedostajućeg root-a strukture, pa se učitavanje po stranama potpuno preskače. Dubinski prolaz ne košta ništa na dokumentima koji od njega ne mogu da profitiraju

Konzervativno po dizajnu: propusti tiho, nikad ne viči vuk

Najvažnije svojstvo ovog validatora je ono što odbija da radi. On poklapa samo standardne /S tipove koje FPDF_StructElement_GetType vraća direktno - Figure, Formula, Note. Dokument koji definiše prilagođeni tip i mapira ga na Figure će, zavisno od toga kako PDFium razreši tip, prijaviti sopstveno ime. Kada se to desi, proverivač ga ne prepoznaje i ostaje tih. To je lažno negativan rezultat, i to je namerno ponašanje. Pravilo dizajna je da under-report rather than ever produce a false positive, because a preflight tool that cries wolf on conformant files trains its users to ignore it — and an ignored validator is worse than none. Decorative images live in the artifact stream, not the structure tree, so they never surface as Figures to begin with; you will not get a "missing Alt" complaint about a background rule that is correctly marked as an artifact

I ovo je razlog što je opseg zadržan na tri pravila. Ugnježđivanje nivoa naslova (klauzula 7.4), obim zaglavlja tabela (7.5) i detekcija ciklusa role map-a (7.1) jesu legitimni PDF/UA zahtevi, ali njihovo dobro proveravanje traži stvarnu analizu grafa i atributa, a njihovo naivno proveravanje proizvodi upravo one lažne pozitivne rezultate koje dizajn zabranjuje - PDF/UA dopušta obrasce naslova poput H1, H2, H3, H3 koje bi jednostavno pravilo "mora strogo da raste" pogrešno odbacilo. Te provere su prepuštene namenskim alatima za usklađenost. Skup Tier-1 je podskup kod koga je nedostajući atribut nedvosmislen

Granica, jasno rečena

Dve granice vredi znati pre nego što ovo ugradite u release gate. Prvo, proverivač je dobar samo onoliko koliko PDFium može da pročita iz elementa strukture. Nekoliko fajlova iz conformance korpusa koje referentni validator prolazi koristi mehanizam alternativnog teksta koji PDFium ne izlaže, pa FPDF_StructElement_GetAltText vraća prazno čak iako je fajl zaista usklađen. Čista provera onda "tačno" prijavi nedostajući Alt na nepotpunim podacima - lažno pozitivan rezultat koji potiče iz pokrivenosti accessor-a u DLL-u, a ne iz logike pravila. Ublažavanje pravila da bi se ti slučajevi progutali takođe bi ga oslepelo za stvarne greške koje treba da hvata, pa su dokumentovani kao poznato ograničenje PDFium-a umesto da se prikrivaju

Drugo, ovo je preflight, a ne sertifikacija. Tier-1 hvata sadržajne greške visoke pouzdanosti koje bajt sken strukturalno može da vidi, i radi to bez lažnih uzbuna - ali puna PDF/UA usklađenost, uključujući semantiku naslova, strukturu tabela i ispravnost redosleda čitanja, i dalje pripada kompletnom validatoru i na kraju ljudskom recenzentu. Koristite ValidatePdfUa da brzo i jeftino oborite očigledne defekte u svom pipeline-u, a zatim pustite veraPDF ili PAC da daju poslednju reč. Ista obilazak stabla strukture stoji iza pravljenja pristupačnog PDF čitača u Delphiju, gde tag stablo upravlja redosledom čitanja i izgovorenim tekstom, i dopunjuje rad na nivou metapodataka u pregledu PDF anotacija iz Delphija

API-ji za stablo strukture i ValidatePdfUa validator prikazani ovde isporučuju se uz PDFium Component za Delphi i C++Builder (VCL) i Lazarus/FPC (LCL). Stranica proizvoda vodi do pune API reference, uključujući kompletan raspored TPdfStructureElement zapisa i enumeraciju problema iza ovih provera