Raportul de preflight spune că fișierul este curat pentru PDF/UA. veraPDF deschide același fișier și semnalează o Figure fără text alternativ la clauza 7.3. Ambele instrumente au dreptate, iar diferența dintre ele este chiar problema verificării accesibilității prin scanarea octeților. O trecere la nivel de octet confirmă că fișierul afirmă că este etichetat: găsește /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part din pachetul XMP, titlul documentului, limba. Acestea sunt marcatori de format și sunt necesari. Nu spun nimic despre faptul dacă figura reală de la pagina patru are o descriere pe care un cititor de ecran o poate citi cu voce tare. Răspunsul se află în arborele de etichete, iar pentru a-l obține trebuie să parcurgi arborele
PDFium Component este o bibliotecă PDF VCL nativă pentru Delphi și C++Builder, iar ValidatePdfUa execută ambele treceri. Trecerea la nivel de octet se ocupă de marcatorii de format. Peste ea se află o trecere prin arborele de structură care încarcă arborele etichetat live, parcurge fiecare element și verifică setul mic de reguli de conținut cu încredere ridicată, acolo unde un atribut lipsă înseamnă un defect real de accesibilitate, nu o preferință de stil. Articolul de față este despre a doua trecere: ce verifică, de ce logica regulilor este o funcție pură fără niciun DLL dedesubt și unde se oprește în mod intenționat
De ce o scanare la nivel de octet nu poate vedea un Alt lipsă
ISO 14289-1 (PDF/UA-1) este un strat de cerințe peste ISO 32000. Unele dintre aceste cerințe sunt structurale și vizibile în fișierul brut: catalogul trebuie să declare un arbore de structură, preferințele vizualizatorului trebuie să seteze DisplayDocTitle, fonturile trebuie să fie încorporate. Un scanner de tokenuri care elimină corpurile de stream și potrivește tokenurile de nume după limitele delimitatoarelor le poate verifica pe toate, iar PDFium's ValidatePdfUaCompliance face exact asta pentru clauze precum 7.1, 7.18 și 7.21
Dar "fiecare Figure are text alternativ" nu este o proprietate a sintaxei fișierului. Este o proprietate a structurii logice - arborele elementelor etichetate care mapează conținutul la sens. Intrarea Alt a unei Figure poate sta în dicționarul elementului de structură, poate fi furnizată printr-un /ActualTextspan/Alt sau poate proveni dintr-un tip personalizat mapat prin rol. Nu o poți găsi în mod fiabil căutând în fluxul de octeți, deoarece acel șir apare în contexte fără legătură, poate fi comprimat într-un object stream și nu îți spune nimic despre cărui element de structură îi aparține. Modul onest de a răspunde la întrebare este să întrebi propriul arbore de structură al documentului, element cu element, aceeași suprafață pe care o evaluează veraPDF și PAC. Pe această linie sunt construite verificările Tier-1 ale PDFium: scanare la nivel de octet pentru format, parcurgere a arborelui pentru conținut
Citirea arborelui de etichete live
Materia primă este TPdf.GetStructureElements (expus și ca StructureElements proprietate), care returnează un TPdfStructureElements - o matrice plată de TPdfStructureElement în ordinea documentului. Fiecare înregistrare este proiecția unui singur element de structură prin funcțiile de acces ale PDFium, cu câmpurile de care au într-adevăr nevoie regulile de accesibilitate
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;
câmpul TypeName este cel pe care se sprijină validatorul. El provine din FPDF_StructElement_GetType, care returnează tipul standard de structură al elementului - numele său /S - după ce PDFium a rezolvat maparea rolurilorAlternateText provine din FPDF_StructElement_GetAltText, ActualText din FPDF_StructElement_GetActualText, iar ID din FPDF_StructElement_GetID. Pentru că matricea este plată și ordonată, validatorul poate raționa asupra întregului document deodată, nu prin recursivitate - lucru important pentru regula care este globală, nu per element
Verificatorul este o funcție pură, și asta este intenționat
Logica regulilor nu trăiește în metoda care vorbește cu DLL-ul. Este o funcție pură, publică, de sine stătătoare:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Primește un vector plat de elemente și returnează un set de probleme. Nu apelează nicio funcție PDFium, nu deschide niciun document, nu atinge nicio stare globală. Această separare este deliberată și dă roade de două ori. Mai întâi, testabilitate: poți construi un TPdfStructureElements vector sintetic într-un test unitar, o Figure fără Alt, o Formula al cărei singur text accesibil este în ActualText, două Note care partajează un ID, și poți verifica setul de rezultate fără pdfium.dll să existe deloc o pagină reală. Logica regulilor este verificată offline; parcurgerea DLL-ului este verificată separat printr-un smoke test pe un document real, care este omis atunci când biblioteca lipsește
În al doilea rând, claritatea responsabilității. TPdf.ValidatePdfUa preia partea grea - încărcarea fiecărei pagini, extragerea elementelor, acumularea lor - și apoi îi pasează un vector curat verificatorului pur. „Ia datele” (DLL, efecte secundare, durată de viață) și „judecă regulile” (pur, determinist) nu se încurcă niciodată. Când o regulă trebuie schimbată, schimbi o funcție care nu are I/O în ea
Ce verifică de fapt cele trei reguli
Parcurgerea arborelui de structură generează trei valori de tip issue, adăugate la finalul TPdfUaValidationIssues astfel încât enum-ul să rămână ABI-stabil pentru apelanții existenți: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, și pvuaiNoteMissingId. Corpul este suficient de mic încât să poată fi înțeles complet:
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;
Clauza 7.3 reglementează figurile: un Figure element trebuie să ofere o alternativă text. Versiunea inițială a acestei verificări se uita doar la intrarea Alt, ceea ce o făcea mai strictă decât validatoarele de referință. PDF/UA acceptă o figură al cărei text accesibil este furnizat prin ActualText în schimb, textul de înlocuire este o reprezentare alternativă validă, așa că regula marchează o Figure doar când ambele Alt și ActualText sunt goale. Clauza 7.7 acoperă formulele și, după aceeași corecție, folosește același test Alt-sau-ActualText; un eșantion din corpusul de conformitate care oferea unei Formula textul accesibil doar prin ActualText era respins în mod fals până când ramura Formula a fost adusă în linie cu ramura Figure
Clauza 7.9 este diferită ca natură. O Note trebuie să aibă un /ID, iar acel ID trebuie să fie unic în întregul document. Un ID lipsă este un eșec la nivel de element. Un duplicat ID este o relație între două elemente, motiv pentru care vectorul plat contează: pentru fiecare Note, verificatorul parcurge înapoi elementele deja văzute și semnalează o coliziune cu orice Note anterioară care poartă același ID. Costul este desigur O(n²) în funcție de numărul de Note, ceea ce este irelevant pentru orice document real și menține funcția ca o singură buclă lizibilă, fără un index auxiliar care să trebuiască sincronizat
Acumularea pe mai multe pagini astfel încât unicitatea să fie globală
PDFium expune elementele structurii pe pagină, nu pe document, așa că orchestrarea din ValidatePdfUa trebuie să le adune înainte ca regulile să ruleze. Parcurge fiecare pagină cu FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, independent de pagina deschisă în acel moment de componentă, și adaugă elementele fiecărei pagini într-un singur vector. Abia apoi apelează verificatorul pur:
// 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;
Acumularea este ceea ce face corectă verificarea de unicitate din 7.9. Două Note de pe pagini diferite pot împărți același ID; dacă ai valida pagină cu pagină, nu ai vedea niciodată coliziunea, deoarece setul de elemente al fiecărei pagini pare intern coerent. Construirea unui singur vector pentru întregul document este singura modalitate prin care dublura devine vizibilă. Și merită menționat și gardianul din față: parcurgerea arborelui rulează doar atunci când trecerea la nivel de octet a nu a raportat pvuaiMissingStructTreeRoot. Un document fără tag-uri nu are niciun arbore de parcurs și a fost deja semnalat pentru rădăcina de structură lipsă, astfel încât încărcările pe pagini sunt sărite complet. Parcurgerea profundă nu costă nimic pentru documentele care nu pot beneficia de ea
Conservator prin design: să rateze în tăcere, niciodată să nu strige lupul
Cea mai importantă proprietate a acestui validator este ceea ce refuză să facă. Potrivește doar numele standard /S de tip pe care FPDF_StructElement_GetType le returnează direct - Figure, Formula, Note. Un document care definește un tip personalizat și îl mapează la Figure va, în funcție de modul în care PDFium rezolvă tipul, să își raporteze propriul nume. Când se întâmplă asta, verificatorul nu îl recunoaște și tace. Acesta este un fals negativ, iar comportamentul este intenționat. Regula de proiectare este să să subraporteze mai degrabă decât să producă vreodată un fals pozitiv, pentru că o unealtă de preflight care strigă lupul la fișiere conforme își obișnuiește utilizatorii să o ignore, iar un validator ignorat e mai rău decât niciunul. Imaginile decorative trăiesc în fluxul de artefacte, nu în arborele de structură, așa că nu apar niciodată ca Figures de la început; nu vei primi o plângere de tipul "missing Alt" pentru o regulă de fundal marcată corect ca artefact
Acesta este și motivul pentru care domeniul rămâne limitat la trei reguli. Înnădirea nivelurilor de heading (clauza 7.4), acoperirea anteturilor de tabel (7.5) și detectarea ciclurilor în role map (7.1) sunt toate cerințe PDF/UA legitime, dar verificarea lor corectă cere o analiză reală a grafului și a atributelor, iar o verificare naivă produce exact falsele pozitive pe care designul le interzice, PDF/UA permite modele de heading precum H1, H2, H3, H3, pe care o regulă simplă de tipul "trebuie să crească strict" le-ar respinge pe nedrept. Aceste verificări sunt lăsate unor instrumente dedicate de conformitate. Setul Tier-1 este subsetul în care un atribut lipsă este neechivoc
Limita, spusă clar
Două limitări merită cunoscute înainte să-l legi de un release gate. În primul rând, verificatorul este doar atât de bun cât poate citi PDFium din elementul de structură. Câteva fișiere din corpusul de conformitate pe care validatoarele de referință le trec folosesc un mecanism de text alternativ pe care PDFium nu îl expune, așa că FPDF_StructElement_GetAltText returnează gol chiar dacă fișierul este cu adevărat conform. Verificatorul pur marchează apoi "corect" un Alt lipsă pe date incomplete, un fals pozitiv care provine din acoperirea accesatorilor din DLL, nu din logica regulilor. Relaxarea regulii pentru a absorbi aceste cazuri l-ar orbi și la eșecurile reale pe care trebuie să le detecteze, așa că ele sunt documentate ca o limitare PDFium cunoscută, nu sunt mascate
În al doilea rând, acesta este un preflight, nu o certificare. Tier-1 prinde erorile de conținut cu grad mare de încredere pe care o scanare pe biți nu le poate prinde structural și o face fără alarme false, însă conformitatea PDF/UA completă, inclusiv semantica headingurilor, structura tabelei și corectitudinea ordinii de citire, rămâne tot în sarcina unui validator complet și, în cele din urmă, a unui recenzor uman. Folosiți ValidatePdfUa pentru a respinge rapid și ieftin defectele evidente în propriul pipeline, apoi lăsați veraPDF sau PAC să aibă ultimul cuvânt. Aceeași parcurgere a arborelui de structură stă la baza construirii unui cititor PDF accesibil în Delphi, unde arborele de tag-uri determină ordinea de citire și textul rostit, și completează munca la nivel de metadate din revizuirea adnotărilor PDF din Delphi
API-urile arborelui de structură și ValidatePdfUavalidatorul prezentat aici se livrează împreună cu PDFium Component pentru Delphi și C++Builder (VCL) și Lazarus/FPC (LCL). Pagina produsului include referința completă a API-ului, inclusiv TPdfStructureElementstructura completă a recordului și enumerarea de probleme din spatele acestor verificări