Il tuo preflight segnala il file come pulito per PDF/UA. veraPDF apre lo stesso file e segnala una Figure senza testo alternativo ai sensi della clausola 7.3. Entrambi gli strumenti hanno ragione, e il divario tra loro è l'intero problema del controllo dell'accessibilità tramite scansione dei byte. Un passaggio a livello di byte conferma che il file dice di essere taggato: trova il /StructTreeRoot, il /MarkInfo /Marked true, il pdfuaid:part nel pacchetto XMP, il titolo del documento, la lingua. Sono marker di formato, e sono necessari. Non ti dicono nulla sul fatto che la figura reale a pagina quattro contenga una descrizione che un lettore di schermo possa leggere ad alta voce. La risposta vive nell'albero dei tag, e per ottenerla devi attraversare l'albero
PDFium Component è una libreria PDF VCL nativa per Delphi e C++Builder, e il suo ValidatePdfUa esegue entrambi i passaggi. Il passaggio a livello di byte gestisce i marker di formato. Sopra di esso c'è un passaggio sull'albero della struttura che carica l'albero taggato vivo, percorre ogni elemento e controlla il piccolo insieme di regole di contenuto ad alta affidabilità in cui un attributo mancante significa un vero difetto di accessibilità anziché una preferenza stilistica. Questo articolo parla di quel secondo passaggio: cosa controlla, perché la logica delle regole è una funzione pura senza alcuna DLL sotto di essa, e dove si ferma deliberatamente prima
Perché una scansione a byte non vede un Alt mancante
ISO 14289-1 (PDF/UA-1) è uno strato di requisiti sopra ISO 32000. Alcuni di questi requisiti sono strutturali e visibili nel file grezzo: il catalogo deve dichiarare un albero della struttura, le preferenze del visualizzatore devono impostare DisplayDocTitle, i font devono essere incorporati. Uno scanner di token che rimuove i corpi degli stream e confronta i token di nome con i confini dei delimitatori può verificarli tutti, e la ValidatePdfUaCompliance fa esattamente questo per clausole come 7.1, 7.18 e 7.21
Ma "every Figure has alternate text" non è una proprietà della sintassi del file. È una proprietà della struttura logica - l'albero degli elementi taggati che mappa il contenuto al significato. La voce Alt di una Figure può trovarsi nel dizionario dell'elemento di struttura, essere fornita tramite uno /ActualText span, o arrivare da un tipo personalizzato mappato tramite una role map. Non puoi trovarla in modo affidabile facendo grep di /Alt nello stream di byte, perché quella stringa compare in contesti non correlati, può essere compressa all'interno di uno stream di oggetti e non ti dice nulla su quale elemento di struttura appartiene. Il modo onesto per rispondere alla domanda è chiedere all'albero della struttura del documento, elemento per elemento, la stessa superficie che valutano veraPDF e PAC. È su questa linea che si basano i controlli Tier-1 di PDFium: scansione a byte per il formato, attraversamento dell'albero per il contenuto
Leggere l'albero dei tag dal vivo
Il materiale grezzo è TPdf.GetStructureElements (esposto anche come la StructureElements proprietà), che restituisce un TPdfStructureElements - un array piatto di TPdfStructureElement record in ordine di documento. Ogni record è la proiezione di un elemento di struttura attraverso le funzioni di accesso di PDFium, con i campi di cui le regole di accessibilità hanno davvero bisogno:
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;
Il TypeName campo è quello su cui il validatore si concentra. Proviene da FPDF_StructElement_GetType, che restituisce il tipo di struttura standard dell'elemento, cioè il suo /S nome, dopo che PDFium ha risolto la role map. AlternateText viene da FPDF_StructElement_GetAltText, ActualText da FPDF_StructElement_GetActualText, e ID da FPDF_StructElement_GetID. Poiché l'array è piatto e ordinato, il validatore può ragionare sull'intero documento in una volta invece di ricorrere, e questo conta per l'unica regola globale anziché per elemento
Il verificatore è una funzione pura, e lo è di proposito
La logica delle regole non vive dentro il metodo che parla con la DLL. È una funzione pura, pubblica e autonoma:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Prende un array piatto di elementi e restituisce un insieme di problemi. Non chiama alcuna funzione di PDFium, non apre alcun documento, non tocca alcuno stato globale. Questa separazione è voluta, e ripaga due volte. Prima di tutto, la testabilità: puoi costruire un TPdfStructureElements array sintetico in un test unitario - una Figure senza Alt, una Formula il cui solo testo accessibile è in ActualText, due Note che condividono un ID - e verificare il set dei risultati senza pdfium.dll senza pdfium.dll presente. La logica delle regole viene verificata offline; l'attraversamento della DLL viene verificato separatamente da uno smoke test su documento reale che salta quando la libreria manca
Secondo, chiarezza delle responsabilità. TPdf.ValidatePdfUa gestisce la parte più sporca, cioè caricare ogni pagina, estrarne gli elementi, accumularli, e poi passa un array pulito al verificatore puro. "Get the data" (DLL, side effects, lifetime) e "judge the rules" (pure, deterministic) non si intrecciano mai. Quando una regola deve cambiare, modifichi una funzione che non contiene I/O
Che cosa controllano davvero le tre regole
Il passaggio sull'albero della struttura produce tre valori di issue, aggiunti in coda a TPdfUaValidationIssues così l'enum resta ABI-stabile per i chiamanti esistenti: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt, e pvuaiNoteMissingId. Il corpo è abbastanza piccolo da poterlo ragionare per intero:
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;
La clausola 7.3 disciplina le figure: una Figure deve fornire un'alternativa testuale. La prima versione di questo controllo guardava solo alla voce Alt, rendendolo più rigido dei validator di riferimento. PDF/UA accetta anche una figure il cui testo accessibile sia fornito tramite ActualText invece, e il testo di sostituzione è una rappresentazione alternativa valida, quindi la regola segnala una Figure solo quando entrambi Alt e ActualText sono vuoti. La clausola 7.7 riguarda le formule e, dopo la stessa correzione, usa lo stesso controllo Alt-or-ActualText; un campione del corpus di conformità che forniva a una Formula il proprio testo accessibile solo tramite ActualText veniva rifiutato per errore finché il ramo Formula non è stato allineato al ramo Figure
La clausola 7.9 è diversa per natura. Una Note deve avere un /ID, e quell'ID deve essere univoco in tutto il documento. Un ID mancante è un errore per elemento. Un duplicato ID è una relazione tra due elementi, ed è per questo che l'array piatto conta: per ogni Note, il verificatore scorre all'indietro gli elementi già visti e segnala una collisione con qualsiasi Note precedente che porti lo stesso ID. Il costo è l'ovvio O(n²) sul numero di Note, irrilevante per qualsiasi documento reale, e mantiene la funzione come un unico ciclo leggibile senza un indice ausiliario da tenere sincronizzato
Accumulo su più pagine così l'unicità è globale
PDFium espone gli elementi della struttura per pagina, non per documento, quindi l'orchestrazione in ValidatePdfUa deve raccoglierli prima che le regole vengano eseguite. Scorre ogni pagina con FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, indipendentemente dalla pagina che il componente ha aperta in quel momento, e aggiunge gli elementi di ogni pagina in un unico array. Solo allora chiama il verificatore puro:
// 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;
L'accumulo è ciò che rende corretto il controllo di unicità 7.9. Due Note su pagine diverse possono condividere un ID; se validassi pagina per pagina non vedresti mai la collisione, perché l'insieme di elementi di ogni pagina sembra internamente coerente. Costruire un solo array esteso all'intero documento è l'unico modo per rendere visibile il duplicato. Vale la pena notare anche il controllo iniziale: l'attraversamento dell'albero viene eseguito solo quando il passaggio a livello di byte non segnala pvuaiMissingStructTreeRoot. Un documento non taggato non ha alcun albero da attraversare ed è già stato segnalato per la mancanza della radice della struttura, quindi i caricamenti per pagina vengono saltati del tutto. Il passaggio profondo non costa nulla sui documenti che non possono trarne beneficio
Conservativo per progettazione: meglio sottosegnalare che produrre falsi positivi
La proprietà più importante di questo validatore è ciò che si rifiuta di fare. Confronta solo i nomi standard /S dei tipi che FPDF_StructElement_GetType restituisce direttamente: Figure, Formula, e Note. Un documento che definisce un tipo personalizzato e lo mappa a Figure, a seconda di come PDFium risolve il tipo, riporterà il proprio nome. Quando succede, il verificatore non lo riconosce e rimane in silenzio. Questo è un falso negativo, ed è il comportamento voluto. La regola di progetto è di sottosegnalare piuttosto che produrre un falso positivo, perché uno strumento di preflight che grida al lupo sui file conformi abitua gli utenti a ignorarlo, e un validatore ignorato è peggio di nessuno. Le immagini decorative vivono nel flusso degli artefatti, non nell'albero della struttura, quindi non emergono mai come Figure in primo luogo; non riceverai un reclamo "missing Alt" per una regola di sfondo correttamente marcata come artifact
Anche questo è il motivo per cui l'ambito resta limitato a tre regole. L'annidamento dei livelli di heading (clausola 7.4), l'ambito degli header delle tabelle (7.5) e il rilevamento dei cicli nella role map (7.1) sono tutti requisiti PDF/UA legittimi, ma controllarli bene richiede una vera analisi del grafo e degli attributi, mentre controllarli in modo ingenuo produce esattamente i falsi positivi che il design vieta. PDF/UA consente schemi di heading come H1, H2, H3, H3 che una semplice regola "devono aumentare in modo stretto" rifiuterebbe per errore. Questi controlli sono lasciati a strumenti di conformità dedicati. Il set Tier-1 è il sottoinsieme in cui un attributo mancante è inequivocabile
Il confine, detto chiaramente
Due limiti valgono la pena di conoscere prima di collegarlo a un gate di rilascio. Prima di tutto, il verificatore è valido solo quanto ciò che PDFium riesce a leggere dall'elemento della struttura. Alcuni file del corpus di conformità che i validator di riferimento superano usano un meccanismo di testo alternativo che PDFium non espone, quindi FPDF_StructElement_GetAltText restituisce vuoto anche se il file è davvero conforme. Il verificatore puro allora "correttamente" segnala un Alt mancante su dati incompleti, un falso positivo che nasce dalla copertura degli accessor della DLL, non dalla logica delle regole. Ammorbidire la regola per assorbire questi casi la renderebbe anche cieca ai veri errori che deve intercettare, quindi vengono documentati come una limitazione nota di PDFium invece di essere mascherati
Secondo, si tratta di un preflight, non di una certificazione. Tier-1 intercetta gli errori di contenuto ad alta confidenza che una scansione a byte non può intercettare strutturalmente, e lo fa senza falsi allarmi, ma la piena conformità PDF/UA, compresa la semantica degli heading, la struttura delle tabelle e la correttezza dell'ordine di lettura, resta comunque compito di un validator completo e, alla fine, di un revisore umano. Usa ValidatePdfUa per fallire velocemente e a basso costo i difetti evidenti nella tua pipeline, poi lascia che veraPDF o PAC abbiano l'ultima parola. Lo stesso attraversamento dell'albero della struttura è alla base della costruzione di un lettore PDF accessibile in Delphi, dove l'albero dei tag guida l'ordine di lettura e il testo parlato, e si affianca al lavoro a livello di metadati in revisione delle annotazioni PDF in Delphi
Le API dell'albero della struttura e il ValidatePdfUa validatore mostrato qui vengono forniti con il PDFium Component per Delphi e C++Builder (VCL) e Lazarus/FPC (LCL). La pagina del prodotto rimanda alla documentazione completa delle API, incluso il layout completo del record TPdfStructureElement e l'enumerazione degli issue alla base di questi controlli