Din förhandskontroll rapporterar att filen är PDF/UA-ren. veraPDF öppnar samma fil och flaggar en Figure utan alternativtext enligt klausul 7.3. Båda verktygen har rätt, och glappet mellan dem är hela problemet med att kontrollera tillgänglighet genom att skanna byte för byte. En kontroll på byte-nivå bekräftar att filen säger att den är taggad: den hittar /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part i XMP-paketet, dokumenttiteln och språket. Dessa är formatmarkörer, och de är nödvändiga. De säger dig ingenting om huruvida den faktiska figuren på sida fyra har en beskrivning som en skärmläsare kan läsa upp. Svaret finns i taggträdet, och för att få fram det måste du gå igenom trädet
PDFium Component är ett inbyggt VCL-PDF-bibliotek för Delphi och C++Builder, och dess ValidatePdfUa gör båda passen. Byte-nivåpasset hanterar formatmarkörerna. Ovanpå det ligger ett strukturträds-pass som läser in det levande taggträdet, går igenom varje element och kontrollerar den lilla uppsättningen innehållsregler med hög säkerhet där ett saknat attribut betyder en verklig tillgänglighetsbrist snarare än en stilpreferens. Den här artikeln handlar om det andra passet: vad det kontrollerar, varför regel-logiken är en ren funktion utan någon DLL under sig, och var den medvetet stannar kort
Varför en bytegenomsökning inte kan se ett saknat Alt
ISO 14289-1 (PDF/UA-1) är ett lager av krav ovanpå ISO 32000. Vissa av de kraven är strukturella och synliga i den råa filen: katalogen måste deklarera ett strukturträd, visningsinställningarna måste sätta DisplayDocTitle, teckensnitt måste vara inbäddade. En token-skanner som tar bort ströminnehåll och matchar namntoken med avgränsare kan verifiera allt detta, och PDFiums ValidatePdfUaCompliance gör just det för klausuler som 7.1, 7.18 och 7.21
Men "att varje Figure har alternativtext" är inte en egenskap hos filens syntax. Det är en egenskap hos den logiska strukturen, alltså trädet av taggade element som mappar innehåll till betydelse. En Figures Alt-post kan ligga i struktur-elementets ordbok, tillhandahållas via ett /ActualText span, eller komma från en rollmappad anpassad typ. Du kan inte tillförlitligt hitta den genom att söka efter /Alt i byteflödet, eftersom den strängen förekommer i orelaterade sammanhang, kan vara komprimerad i en objektström och inte säger dig något om vilket struktur-element den hör till. Det ärliga sättet att besvara frågan är att fråga dokumentets eget strukturträd, element för element, samma yta som veraPDF och PAC utvärderar. Det är den linje som PDFiums Tier-1-kontroller byggs kring: bytegenomsökning för format, trädpromenad för innehåll
Läser det levande taggträdet
Råmaterialet är TPdf.GetStructureElements (även exponerat som StructureElements egenskapen), som returnerar en TPdfStructureElements - en platt array av TPdfStructureElement poster i dokumentordning. Varje post är projektionen av ett struktur-element genom PDFiums åtkomstfunktioner, med de fält som tillgänglighetsreglerna faktiskt behöver:
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;
Fältet TypeName är det som valideraren utgår från. Det kommer från FPDF_StructElement_GetType, som returnerar elementets standardstrukturtyp, dess /S-namn, efter att PDFium har löst rollmappen. AlternateText kommer från FPDF_StructElement_GetAltText, ActualText från FPDF_StructElement_GetActualText, och ID från FPDF_StructElement_GetID. Eftersom arrayen är platt och ordnad kan valideraren resonera om hela dokumentet på en gång i stället för att gå rekursivt, vilket spelar roll för den enda regel som är global snarare än per element
Kontrollen är en ren funktion, och det är medvetet
Regellogiken lever inte inne i metoden som pratar med DLL:en. Den är en fristående, offentlig, ren funktion:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Den tar en platt array av element och returnerar en uppsättning problem. Den anropar ingen PDFium-funktion, öppnar inget dokument, rör ingen global state. Den uppdelningen är medveten, och den ger utdelning på två sätt. För det första, testbarhet: du kan bygga en syntetisk TPdfStructureElements array i ett enhetstest - en Figure utan Alt, en Formula vars enda tillgängliga text ligger i ActualText, två Notes som delar ett ID - och hävda på resultatmängden utan att pdfium.dll ens finns på plats. Regellogiken verifieras offline; DLL-genomgången verifieras separat med ett smoke-test mot ett live-dokument som hoppas över när biblioteket saknas
För det andra, tydlig ansvarsfördelning. TPdf.ValidatePdfUa äger den röriga delen, att ladda varje sida, hämta dess element, samla ihop dem, och lämnar sedan en ren array till den rena kontrollen. "Hämta data" (DLL, sidoeffekter, livscykel) och "bedöm reglerna" (ren, deterministisk) blandas aldrig ihop. När en regel behöver ändras ändrar du en funktion som inte har någon I/O i sig
Vad de tre reglerna faktiskt kontrollerar
Strukturträdsgenomgången ger tre issue-värden, som läggs sist i TPdfUaValidationIssues så att enumen förblir ABI-stabil för befintliga anropare: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, och pvuaiNoteMissingId. Brödtexten är tillräckligt liten för att kunna överblickas helt:
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;
Klausul 7.3 gäller Figures: ett Figure element måste ge en textalternativ. Den tidiga versionen av den här kontrollen tittade bara på Alt-fältet, vilket gjorde den striktare än referensvaliderarna. PDF/UA accepterar en Figure vars tillgängliga text tillhandahålls via ActualText i stället, ersättningstext är en giltig alternativ representation, så regeln markerar bara en Figure när båda Alt och ActualText är tomma. Klausul 7.7 gäller Formulas, och efter samma korrigering använder den identiska Alt-eller-ActualText-kontrollen; ett prov i överensstämmelsekorpuset som gav en Formula sin tillgängliga text enbart via ActualText avvisades felaktigt tills Formula-grenen anpassades till Figure-grenen
Klausul 7.9 är annorlunda till sin karaktär. En NoteNote/ID måste ha ett duplicerat ID:t måste vara unikt över hela dokumentet. Ett saknat ID är ett fel på elementnivå. Ett
ID är en relation mellan två element, vilket är anledningen till att den platta arrayen spelar roll: för varje Note skannar kontrollen bakåt över de element som redan setts och flaggar en kollision med varje tidigare Note som har samma ID. Kostnaden är den uppenbara O(n²) över antalet Note, vilket är irrelevant för alla verkliga dokument och håller funktionen till en enda läsbar loop utan något hjälpindex att hålla synkroniserat
Samlar över sidor så att unikheten blir globalValidatePdfUaPDFium exponerar struktur-element per sida, inte per dokument, så samordningen i FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, oberoende av vilken sida komponenten råkar ha öppen, och lägger till varje sidas element i en enda array. Först därefter anropar den den rena kontrollen:
// 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;
Sammanslagningen är det som gör 7.9-kontrollen för unikhet korrekt. Två Notes på olika sidor kan dela ett ID; om du validerade sida för sida skulle du aldrig se kollisionen, eftersom varje sidas elementuppsättning ser internkonsekvent ut. Att bygga en enda dokumentomfattande array är det enda sättet att göra dupliceringen synlig. Skyddet i början är också värt att notera: trädpromenaden körs bara när bytepassagen inte rapporterade inte rapporterade pvuaiMissingStructTreeRoot. Ett otaggat dokument har inget träd att gå igenom och har redan flaggats för saknad strukturrot, så sidladdningarna hoppas över helt. Det djupare passet kostar inget på dokument som inte kan dra nytta av det
Konservativ som designval: missa tyst, ropa aldrig varg
Den enskilt viktigaste egenskapen hos den här valideraren är vad den vägrar att göra. Den matchar bara de standardiserade /S typnamn som FPDF_StructElement_GetType returnerar direkt, Figure, Formula, Note. Ett dokument som definierar en egen typ och rollmappar den till Figure kommer, beroende på hur PDFium löser typen, att rapportera sitt eget namn. När det händer känner kontrollen inte igen det och förblir tyst. Det är en falsk negativ, och det är avsiktligt beteende. Designregeln är att underrapportera i stället för att någonsin ge en falsk positiv, eftersom ett preflight-verktyg som ropar varg på korrekta filer lär sina användare att ignorera det, och en ignorerad validerare är sämre än ingen alls. Dekorativa bilder ligger i artefaktströmmen, inte i strukturträdet, så de dyker aldrig upp som Figures från början, och du får ingen "saknat Alt"-anmärkning för en bakgrundslinje som är korrekt markerad som artefakt
Det är också därför omfånget hålls till tre regler. Rubriknivånästning (klausul 7.4), tabellhuvudets omfång (7.5) och detektering av rollmapscykler (7.1) är alla legitima PDF/UA-krav, men att kontrollera dem väl kräver riktig graf- och attributanalys, och att kontrollera dem naivt ger exakt de falska positiva som designen förbjuder. PDF/UA tillåter rubrikmönster som H1, H2, H3, H3 som en enkel regel om att nivåerna måste öka strikt skulle förkasta felaktigt. De kontrollerna lämnas till särskilda överensstämmelseverktyg. Tier-1-uppsättningen är den del där ett saknat attribut är entydigt
Gränsen, uttryckt rakt ut
Två begränsningar är värda att känna till innan du kopplar in det här i en release-grind. För det första är kontrollen bara så bra som det PDFium kan läsa från struktur-elementet. Några filer i överensstämmelsekorpuset som referensvaliderarna klarar använder en mekanism för alternativtext som PDFium inte exponerar, så FPDF_StructElement_GetAltText returnerar tomt trots att filen faktiskt är konform. Den rena kontrollen markerar då "korrekt" ett saknat Alt på ofullständiga data, en falsk positiv som kommer från DLL:ens åtkomsttäckning, inte från regellogiken. Att mjuka upp regeln för att sluka de fallen skulle också göra den blind för de verkliga fel den är tänkt att fånga, så de dokumenteras som en känd PDFium-begränsning i stället för att döljas
För det andra är det här en förhandskontroll, inte en certifiering. Tier-1 fångar de innehållsfel med hög säkerhet som en bytegenomsökning strukturellt sett inte kan hitta, och gör det utan falsklarm, men full PDF/UA-överensstämmelse, inklusive huvudsemantik, tabellstruktur och korrekt läsordning, hör fortfarande hemma hos en komplett validerare och i slutänden hos en mänsklig granskare. Använd ValidatePdfUa för att stoppa de uppenbara felen snabbt och billigt i din egen pipeline, och låt sedan veraPDF eller PAC få sista ordet. Samma strukturträdsgenomgång ligger också bakom att bygga en tillgänglig PDF-läsare i Delphi, där taggträdet styr läsordningen och det upplästa innehållet, och den kompletterar arbetet på metadatanivå i granskning av PDF-annoteringar i Delphi
Strukturträds-API:erna och ValidatePdfUa valideraren som visas här levereras med PDFium Component för Delphi och C++Builder (VCL) samt Lazarus/FPC (LCL). Produktsidan länkar till hela API-referensen, inklusive den fullständiga TPdfStructureElement-recordlayouten och issue-enumerationen bakom dessa kontroller