Tehnički članak

Provjera strukture stabla PDF/UA u Delphiju uz PDFium

Vaš preflight prijavljuje datoteku kao čistu za PDF/UA. veraPDF otvara istu datoteku i označava Figure bez alternativnog teksta prema članku 7.3. Oba alata su u pravu, a jaz između njih je cijeli problem provjere pristupačnosti skeniranjem bajtova. Byte-level prolaz potvrđuje da datoteka kaže da je označena: pronalazi <code>/StructTreeRoot</code>, <code>/MarkInfo /Marked true</code>, <code>pdfuaid:part</code> u XMP paketu, naslov dokumenta, jezik. To su formatni markeri i oni su nužni. Ne govore vam ništa o tome nosi li stvarna figura na četvrtoj stranici opis koji čitač zaslona može pročitati naglas. Taj odgovor živi u tag stablu, a da biste ga dobili morate proći kroz stablo.PDFium Component je nativna VCL PDF biblioteka za Delphi i C++Builder, a njezin ValidatePdfUa radi oba prolaza. Byte-level prolaz obrađuje formatne markere. Iznad njega stoji prolaz kroz strukturalno stablo koji učitava živo označeno stablo, prolazi kroz svaki element i provjerava mali skup visokopouzdane sadržajne pravila gdje nedostajući atribut znači stvarni pristupni nedostatak, a ne stilsko pitanje. Ovaj članak govori o tom drugom prolazu: što provjerava, zašto je logika pravila čista funkcija bez DLL-a ispod nje, i gdje namjerno staje./StructTreeRoot, the /MarkInfo /Marked true, the pdfuaid:part in the XMP packet, the document title, the language. Those are format markers, and they are necessary. They tell you nothing about whether the actual figure on page four carries a description a screen reader can read aloud. That answer lives in the tag tree, and to get it you have to walk the tree

PDFium Component is a native VCL PDF library for Delphi and C++Builder, and its ValidatePdfUa does both passes. The byte-level pass handles the format markers. On top of it sits a structure-tree pass that loads the live tagged tree, walks every element, and checks the small set of high-confidence content rules where a missing attribute means a real accessibility defect rather than a stylistic preference. This article is about that second pass: what it checks, why the rule logic is a pure function with no DLL underneath it, and where it deliberately stops short

Zašto byte scan ne može vidjeti nedostajući Alt

ISO 14289-1 (PDF/UA-1) je sloj zahtjeva nad ISO 32000. Neki od tih zahtjeva su strukturni i vidljivi u sirovoj datoteci: katalog mora deklarirati strukturu stabla, viewer preferences moraju postaviti DisplayDocTitle, fonts must be embedded. A token scanner that strips stream bodies and matches name tokens with delimiter boundaries can verify all of those, and PDFium's ValidatePdfUaCompliance radi upravo to za članke kao što su 7.1, 7.18 i 7.21

Ali "svaka Figure ima alternativni tekst" nije svojstvo sintakse datoteke. To je svojstvo logičke strukture - stabla označenih elemenata koji mapira sadržaj na značenje. Alt unos Figure može se nalaziti u rječniku strukturalnog elementa, može se isporučiti kroz /ActualText span ili dolaziti iz prilagođenog tipa mapiranog preko role mape. Ne možete ga pouzdano pronaći grepom za /Alt u byte streamu, jer se taj niz pojavljuje u nepovezanim kontekstima, može biti komprimiran unutar object streama, i ne govori vam ništa o tome kojem strukturalnom elementu pripada. Pošten način da se odgovori na to pitanje jest pitati vlastito strukturno stablo dokumenta, element po element, isto sučelje koje evaluiraju veraPDF i PAC. To je linija oko koje su građene PDFiumove provjere Tier-1: byte scan za format, prolaz kroz stablo za sadržaj

Čitanje živog tag stabla

Sirovina je TPdf.GetStructureElements (također izloženo kao StructureElements svojstvo), koje vraća TPdfStructureElements - ravni niz od TPdfStructureElement zapisa u redoslijedu dokumenta. Svaki zapis je projekcija jednog strukturalnog elementa kroz PDFiumove accessor funkcije, s poljima koja pravila pristupačnosti doista trebaju:

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. Dolazi iz FPDF_StructElement_GetType, koji vraća standardni strukturalni tip elementa - njegovo /S ime - nakon što PDFium riješi role map. AlternateText dolazi iz FPDF_StructElement_GetAltText, ActualText iz FPDF_StructElement_GetActualText, a ID iz FPDF_StructElement_GetID. Budući da je niz ravan i uređen, validator može promišljati o cijelom dokumentu odjednom umjesto rekurzije - što je važno za jedno pravilo koje je globalno, a ne po elementu

Provjera je čista funkcija, i to namjerno

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

function ValidatePdfUaStructureElements(
  const Elements: TPdfStructureElements): TPdfUaValidationIssues;

Prima ravni niz elemenata i vraća skup problema. Ne poziva nijednu PDFium funkciju, ne otvara nijedan dokument, ne dira globalno stanje. Ta je odvojenost namjerna i isplati se dvaput. Prvo, testabilnost: možete izgraditi sintetički TPdfStructureElements niz u unit testu - Figure bez Alt-a, Formula čiji je jedini pristupačni tekst u ActualTextu, dvije Note koje dijele ID - i provjeriti skup rezultata bez pdfium.dll prisutnog uopće. Logika pravila provjerava se offline; DLL traversal provjerava se zasebno pomoću smoke testa na živom dokumentu koji preskače kada biblioteka nedostaje

Drugo, jasnoća odgovornosti. TPdf.ValidatePdfUa upravlja neurednim dijelom - učitavanjem svake stranice, izvlačenjem njezinih elemenata, njihova akumuliranja - a zatim predaje čist niz čistoj provjeri. "Get the data" (DLL, side effects, lifetime) i "judge the rules" (pure, deterministic) nikad se ne miješaju. Kad pravilo treba promjenu, mijenjate funkciju koja u sebi nema I/O

Što tri pravila zapravo provjeravaju

Prolaz kroz strukturalno stablo vraća tri vrijednosti problema, dodane na kraj TPdfUaValidationIssues tako da enum ostane ABI-stabilan za postojeće pozivatelje: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, i pvuaiNoteMissingId. Tijelo je dovoljno malo da ga se može potpuno razumjeti:

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;

Članak 7.3 uređuje figure: Figure element mora pružiti tekstualnu alternativu. Rana verzija ove provjere gledala je samo Alt unos, što ju je činilo strožom od referentnih validatora. PDF/UA prihvaća figuru čiji je pristupačni tekst isporučen kroz ActualText umjesto toga - zamjenski tekst je valjana alternativna reprezentacija - pa pravilo označava Figure samo kada su oba Alt i ActualText prazna. Članak 7.7 pokriva formule, a nakon iste korekcije koristi isti Alt-ili-ActualText test; uzorak iz conformance korpusa koji je formuli dao pristupačni tekst isključivo kroz ActualText bio je pogrešno odbijan sve dok grana za Formulu nije dovedena u red s granom za Figure

Članak 7.9 drugačiji je po prirodi. Note mora imati /ID, a taj ID mora biti jedinstven kroz cijeli dokument. Nedostajući ID je neuspjeh po elementu. Duplicirani ID je odnos između dva elementa, zbog čega je ravni niz važan: za svaku Note, provjera se vraća unatrag kroz elemente koje je već vidjela i označava sudar s bilo kojom ranijom Note koja nosi isti ID. Trošak je očit O(n²) po broju Noteova, što je nebitno za svaki stvarni dokument i zadržava funkciju kao jednu čitljivu petlju bez pomoćnog indeksa kojeg treba održavati sinkroniziranim

Akumuliranje po stranicama tako da jedinstvenost bude globalna

PDFium izlaže strukturalne elemente po stranici, a ne po dokumentu, pa ih orkestracija u ValidatePdfUa mora prikupiti prije nego što pravila krenu. Ona prolazi svaku stranicu s FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, neovisno o tome koju stranicu komponenta trenutačno ima otvorenu, i dodaje elemente svake stranice u jedan niz. Tek tada poziva čistu provjeru:

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

Akumuliranje je ono što čini 7.9 provjeru jedinstvenosti točnom. Dvije Note na različitim stranicama mogu dijeliti ID; ako biste validirali stranicu po stranicu, nikad ne biste vidjeli sudar, jer svaki skup elemenata stranice izgleda interno dosljedno. Izgradnja jednog dokumentskog niza jedini je način da se duplikat pokaže. Vrijedi primijetiti i zaštitu na početku: prolaz kroz stablo pokreće se samo kada byte-level prolaz nije prijavio pvuaiMissingStructTreeRoot prijavio pvuaiMissingStructTreeRoot. Neoznačeni dokument nema stablo kroz koje bi se prolazilo i već je označen zbog nedostajućeg korijena strukture, pa se učitavanje po stranicama potpuno preskače. Duboki prolaz ne košta ništa na dokumentima koji od njega ne mogu profitirati

Konzervativno po dizajnu: promaši tiho, nikad ne viči vuka

Najvažnije svojstvo ovog validatora je ono što odbija raditi. On podudara samo standardne /S tipove naziva koje FPDF_StructElement_GetType vraća izravno - Figure, Formula, Note. Dokument koji definira prilagođeni tip i role-mapa ga na Figure, ovisno o tome kako PDFium razriješi tip, prijavit će vlastito ime. Kad se to dogodi, provjera ga ne prepoznaje i ostaje tiha. To je false negative, i to je namjerno ponašanje. Pravilo dizajna je radije premalo prijaviti nego ikad proizvesti lažno pozitivno, jer preflight alat koji viče na usklađene datoteke uči korisnike da ga ignoriraju - a ignorirani validator gori je od nikakvog. Dekorativne slike žive u artifact streamu, ne u strukturalnom stablu, pa se uopće ne pojavljuju kao Figure; nećete dobiti "missing Alt" prigovor za pozadinsku liniju koja je ispravno označena kao artifact

To je također razlog zašto je opseg zadržan na tri pravila. Ugnježđivanje razine naslova (članak 7.4), opseg zaglavlja tablice (7.5) i detekcija ciklusa role mape (7.1) svi su legitimni PDF/UA zahtjevi, ali njihova dobra provjera traži stvarnu analizu grafa i atributa, a naivna provjera 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 rasti" pogrešno odbacilo. Te se provjere prepuštaju namjenskim alatima za usklađenost. Tier-1 skup je podskup gdje je nedostajući atribut nedvosmislen

Granica, jasno rečena

Dvije granice vrijedi znati prije nego što ovo uvežete u release gate. Prvo, provjera je dobra samo koliko i ono što PDFium može pročitati iz strukturalnog elementa. Nekoliko datoteka iz conformance korpusa koje prolaze kroz referentne validatore koristi mehanizam alternativnog teksta koji PDFium ne izlaže, pa FPDF_StructElement_GetAltText vraća prazno iako je datoteka doista usklađena. Čista provjera tada "ispravno" označava nedostajući Alt na nepotpunim podacima - lažno pozitivno koje potječe iz pokrivenosti accessora u DLL-u, a ne iz logike pravila. Opuštanje pravila da bi se ti slučajevi progutali također bi je zaslijepilo za stvarne kvarove koje treba uhvatiti, pa su dokumentirani kao poznato ograničenje PDFiuma umjesto da se prikriju

Drugo, ovo je preflight, ne certifikacija. Tier-1 hvata visokopouzdane sadržajne greške koje byte scan strukturno ne može, i to bez lažnih alarma - ali potpuna PDF/UA usklađenost, uključujući semantiku naslova, strukturu tablice i ispravnost redoslijeda čitanja, i dalje pripada potpunom validatoru i na kraju ljudskom recenzentu. Koristite ValidatePdfUa da brzo i jeftino padnu očite greške u vlastitom pipelineu, a zatim prepustite konačnu riječ veraPDF-u ili PAC-u. Isti prolaz kroz strukturalno stablo podupire izradu u Delphiju, gdje tag stablo upravlja redoslijedom čitanja i govorenim tekstom, a nadopunjuje metapodatkovni rad u pregledu PDF anotacija iz Delphija

API-ji strukturalnog stabla i ValidatePdfUa validator prikazan ovdje dolaze s PDFium Componentom za Delphi i C++Builder (VCL) i Lazarus/FPC (LCL). Stranica proizvoda povezuje potpunu API referencu, uključujući cjeloviti TPdfStructureElement raspored zapisa i enumeraciju problema iza ovih provjera