Din preflight melder filen som PDF/UA-ren. veraPDF åbner den samme fil og markerer en Figure uden alternativ tekst under klausul 7.3. Begge værktøjer har ret, og afstanden mellem dem er hele problemet med at kontrollere tilgængelighed ved at scanne bytes. Et byte-niveau-pas bekræfter, at filen siger, at den er tagget: den finder /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part i XMP-pakken, dokumenttitlen, sproget. De er formatmarkører, og de er nødvendige. De fortæller dig intet om, hvorvidt selve figuren på side fire bærer en beskrivelse, en skærmlæser kan læse højt. Det svar findes i tag-træet, og for at få det skal du gennemgå træet
PDFium Component er et native VCL PDF-bibliotek til Delphi og C++Builder, og dets ValidatePdfUa udfører begge pas. Byte-niveau-passet håndterer formatmarkørerne. Oven på det ligger et strukturtræ-pas, der indlæser det levende taggede træ, gennemgår hvert element og tjekker det lille sæt højsikkerheds-indholdsregler, hvor et manglende attribut betyder en reel tilgængelighedsfejl frem for en stilistisk præference. Denne artikel handler om det andet pas: hvad det tjekker, hvorfor regellogikken er en ren funktion uden nogen DLL under sig, og hvor den bevidst stopper
Hvorfor en bytescanning ikke kan se en manglende Alt
ISO 14289-1 (PDF/UA-1) er et lag af krav oven på ISO 32000. Nogle af de krav er strukturelle og synlige i den rå fil: kataloget skal erklære et strukturtræ, viewer-præferencerne skal sætte DisplayDocTitle, fonte skal være indlejrede. En token-scanner, der strips stream-kroppe og matcher navnetokens med afgrænsergrænser, kan verificere alt det, og PDFiums ValidatePdfUaCompliance gør præcis det for klausuler som 7.1, 7.18 og 7.21
Men "hver Figure har alternativ tekst" er ikke en egenskab ved filens syntaks. Det er en egenskab ved den logiske struktur — træet af taggede elementer, der mapper indhold til betydning. En Figures Alt-post kan sidde i strukturelementets dictionary, blive leveret gennem en /ActualText-span, eller komme fra en rollemappet brugerdefineret type. Du kan ikke pålideligt finde den ved at grepe efter /Alt i bytestrømmen, fordi den streng optræder i urelaterede kontekster, kan være komprimeret inde i en objekt-stream, og fortæller dig intet om, hvilket strukturelement den tilhører. Den ærlige måde at besvare spørgsmålet på er at spørge dokumentets eget strukturtræ, element for element, den samme overflade veraPDF og PAC evaluerer. Det er den linje, PDFiums Tier-1-tjek er bygget omkring: bytescanning for format, træ-gennemgang for indhold
Læs det levende tagtræ
Råmaterialet er TPdf.GetStructureElements (også eksponeret som egenskaben StructureElements), som returnerer et TPdfStructureElements — et fladt array af TPdfStructureElement-records i dokumentrækkefølge. Hver record er projektionen af ét strukturelement gennem PDFiums accessor-funktioner, med de felter, tilgængelighedsreglerne rent faktisk har brug for:
type
TPdfStructureElement = record
Level: Integer; // dybde i tag-træet
ParentIndex: Integer; // indeks for forælder-element, eller -1
TypeName: WString; // standard /S-navn: 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;
// ... underliggende bogføringsfelter for børn
end;
Feltet TypeName er det, validatoren drejer sig om. Det kommer fra FPDF_StructElement_GetType, som returnerer elementets standard-strukturtype — dets /S-navn — efter PDFium har opløst rollemappet. AlternateText kommer fra FPDF_StructElement_GetAltText, ActualText fra FPDF_StructElement_GetActualText, og ID fra FPDF_StructElement_GetID. Fordi arrayet er fladt og ordnet, kan validatoren ræsonnere om hele dokumentet på én gang i stedet for at rekursere — hvilket betyder noget for den ene regel, der er global frem for pr.-element
Kontrollen er en ren funktion, og det er med vilje
Regellogikken bor ikke inde i metoden, der taler med DLL'en. Det er en selvstændig, offentlig, ren funktion:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Den tager et fladt element-array og returnerer et sæt problemer. Den kalder ingen PDFium-funktion, åbner intet dokument, rører ingen global tilstand. Den adskillelse er bevidst, og den betaler sig to gange. For det første testbarhed: du kan bygge et syntetisk TPdfStructureElements-array i en enhedstest — en Figure uden Alt, en Formula, hvis eneste tilgængelige tekst er i ActualText, to Notes, der deler et ID — og teste resultatsættet uden pdfium.dll til stede overhovedet. Regellogikken verificeres offline; DLL-gennemgangen verificeres separat af en live-dokument-smoke-test, der springes over, når biblioteket mangler
For det andet ansvarsklarhed. TPdf.ValidatePdfUa ejer den rodede del — indlæse hver side, trække dens elementer ud, akkumulere dem — og giver derefter et rent array videre til den rene checker. "Hent dataene" (DLL, sideeffekter, levetid) og "bedøm reglerne" (rent, deterministisk) filtrer aldrig sammen. Skal en regel ændres, ændrer du en funktion, der ikke har nogen I/O i sig
Hvad de tre regler faktisk kontrollerer
Strukturtræ-passet rejser tre problemværdier, tilføjet til slutningen af TPdfUaValidationIssues, så enumen forbliver ABI-stabil for eksisterende kaldere: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt og pvuaiNoteMissingId. Kroppen er lille nok til at ræsonnere fuldt ud om:
for I := 0 to High(Elements) do
begin
T := string(Elements[I].TypeName);
if T = 'Figure' then
begin
// §7.3 — en Figure har brug for en alternativ repræsentation:
// en Alt-post ELLER ActualText. Flag kun, når BEGGE er tomme.
if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
Include(Result, pvuaiFigureMissingAlt);
end
else if T = 'Formula' then
begin
// §7.7 — samme regel som Figure: Alt eller ActualText.
if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
Include(Result, pvuaiFormulaMissingAlt);
end
else if T = 'Note' then
begin
// §7.9 — hver Note skal have et unikt 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;
Klausul 7.3 styrer figurer: et Figure-element skal levere et tekstalternativ. Den tidlige version af dette tjek kiggede kun på Alt-posten, hvilket gjorde det strengere end referencevalidatorerne. PDF/UA accepterer en figur, hvis tilgængelige tekst i stedet leveres gennem ActualText — erstatningstekst er en gyldig alternativ repræsentation — så reglen flager kun en Figure, når begge Alt og ActualText er tomme. Klausul 7.7 dækker formler, og efter samme rettelse bruger den den identiske Alt-eller-ActualText-test; en conformance-corpus-prøve, der gav en Formula dens tilgængelige tekst udelukkende gennem ActualText, blev fejlagtigt afvist, indtil Formula-grenen blev bragt på linje med Figure-grenen
Klausul 7.9 er en anden slags. En Note skal have et /ID, og det ID skal være unikt på tværs af dokumentet. Et manglende ID er en pr.-element-fejl. Et duplikeret ID er en relation mellem to elementer, hvilket er grunden til, at det flade array betyder noget: for hver Note scanner checkeren bagud over de allerede sete elementer og flager en kollision med enhver tidligere Note, der bærer det samme ID. Prisen er det oplagte O(n²) over Note-antal, hvilket er irrelevant for ethvert reelt dokument og holder funktionen som én læsbar løkke uden noget hjælpeindeks at holde synkroniseret
Akkumulér på tværs af sider, så unikhed er global
PDFium eksponerer strukturelementer pr. side, ikke pr. dokument, så orkestreringen i ValidatePdfUa skal indsamle dem, før reglerne kører. Den gennemgår hver side med FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, uafhængigt af hvilken side komponenten aktuelt har åben, og tilføjer hver sides elementer til ét array. Først derefter kalder den den rene checker:
// inde i TPdf.ValidatePdfUa, efter byte-niveau-passet
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;
// tilføj PageElems til AllElems ...
end;
Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;
Akkumuleringen er, hvad der gør 7.9-unikhedstjekket korrekt. To Notes på forskellige sider kan dele et ID; validerede du side for side, ville du aldrig se kollisionen, fordi hver sides elementsæt ser internt konsistent ud. At bygge ét dokumentbredt array er den eneste måde, duplikatet bliver synligt. Vagten foran er også værd at bemærke: træ-gennemgangen kører kun, når byte-niveau-passet ikke rapporterede pvuaiMissingStructTreeRoot. Et utagget dokument har intet træ at gennemgå og er allerede blevet flaget for det manglende strukturrod, så de pr.-side-indlæsninger springes helt over. Det dybe pas koster intet på de dokumenter, der ikke kan drage nytte af det
Konservativ af design: overser stille, råber aldrig ulv
Den enkelt vigtigste egenskab ved denne validator er det, den nægter at gøre. Den matcher kun de standard-/S-typenavne, som FPDF_StructElement_GetType returnerer direkte — Figure, Formula, Note. Et dokument, der definerer en brugerdefineret type og rollemapper den til Figure, vil, afhængigt af hvordan PDFium opløser typen, rapportere sit eget navn. Når det sker, genkender checkeren den ikke og forbliver stille. Det er et falsk negativ, og det er den tilsigtede adfærd. Designreglen er at underrapportere frem for nogensinde at producere et falsk positivt, fordi et preflight-værktøj, der råber ulv på konforme filer, træner sine brugere til at ignorere det — og en ignoreret validator er værre end ingen. Dekorative billeder ligger i artifact-streamen, ikke strukturtræet, så de dukker aldrig op som Figures overhovedet; du får ikke en "manglende Alt"-klage over en baggrundsregel, der korrekt er markeret som et artifact
Det er også grunden til, at omfanget holdes til tre regler. Overskriftsniveau-indlejring (klausul 7.4), tabel-header-scope (7.5) og rollemap-cyklusdetektion (7.1) er alle legitime PDF/UA-krav, men at tjekke dem godt kræver reel graf- og attributanalyse, og at tjekke dem naivt producerer præcis de falske positiver, designet forbyder — PDF/UA tillader overskriftsmønstre som H1, H2, H3, H3, som en simpel "skal strengt stige"-regel forkert ville afvise. De tjek overlades til dedikerede conformance-værktøjer. Tier-1-sættet er delmængden, hvor et manglende attribut er entydigt
Grænsen, sagt ligeud
To grænser er værd at kende, før du kobler dette ind i en udgivelsesport. For det første er checkeren kun så god som det, PDFium kan læse fra strukturelementet. En håndfuld conformance-corpus-filer, som referencevalidatorerne godkender, bruger en alternativ-tekst-mekanisme, PDFium ikke eksponerer, så FPDF_StructElement_GetAltText returnerer tomt, selvom filen reelt er konform. Den rene checker flager så "korrekt" en manglende Alt på ufuldstændige data — et falsk positivt, der stammer fra DLL'ens accessor-dækning, ikke regellogikken. At løsne reglen for at absorbere de tilfælde ville også gøre den blind for de reelle fejl, den er ment til at fange, så de er dokumenteret som en kendt PDFium-begrænsning i stedet for at blive dækket over
For det andet er dette en preflight, ikke en certificering. Tier-1 fanger de højsikkerheds-indholdsfejl, en bytescanning strukturelt ikke kan, og den gør det uden falske alarmer — men fuld PDF/UA-konformitet, inklusive overskriftssemantik, tabelstruktur og korrekt læserækkefølge, hører stadig til en komplet validator og i sidste ende en menneskelig gennemlæser. Brug ValidatePdfUa til at fejle de oplagte defekter hurtigt og billigt i din egen pipeline, og lad så veraPDF eller PAC have det sidste ord. Den samme strukturtræ-gennemgang understøtter opbygningen af en tilgængelig PDF-læser i Delphi, hvor tag-træet driver læserækkefølge og talt tekst, og den komplementerer metadataniveau-arbejdet i gennemgang af PDF-annotationer fra Delphi
Strukturtræ-API'erne og ValidatePdfUa-validatoren, der er vist her, følger med PDFium-komponenten til Delphi og C++Builder (VCL) og Lazarus/FPC (LCL). Produktsiden linker til den fulde API-reference, inklusive det komplette TPdfStructureElement-record-layout og problem-enumeringen bag disse tjek