O relatório de preflight diz que o ficheiro está limpo em PDF/UA. O veraPDF abre o mesmo ficheiro e assinala uma Figure sem texto alternativo ao abrigo da cláusula 7.3. Os dois estão certos, e a diferença entre eles é precisamente o problema de verificar acessibilidade apenas por varrimento
PDFium Component é uma biblioteca PDF nativa VCL para Delphi e C++Builder, e o seu ValidatePdfUa faz as duas passagens. A passagem ao nível dos bytes trata dos marcadores de formato. Em cima dela existe uma passagem pela árvore de estrutura que carrega a árvore etiquetada viva, percorre cada página e valida o conteúdo contra as regras PDF/UA que um simples scan não consegue ver. Este artigo explica o que essa segunda passagem vê de facto, por que razão a validação em duas fases é diferente de um preflight completo, e onde a implementação deliberadamente se fica por três regras
Porque um scan de bytes não vê um Alt em falta
ISO 14289-1 (PDF/UA-1) é uma camada de requisitos sobre o ISO 32000. Alguns desses requisitos são estruturais e visíveis no ficheiro bruto: o catálogo tem de declarar uma árvore de estrutura, as preferências do visualizador têm de definir DisplayDocTitle, as fontes têm de ser incorporadas. Outros vivem acima da sintaxe. A alternância correcta de texto e estrutura significa que um scan de bytes pode confirmar a presença de uma árvore etiquetada, mas não pode provar que o conteúdo daquela árvore faz sentido
Mas «cada Figure tem texto alternativo» não é uma propriedade da sintaxe do ficheiro. É uma propriedade da estrutura lógica, a árvore de elementos etiquetados que mapeia conteúdo para significado. A entrada Alt de uma Figure pode estar no dicionário do elemento de estrutura ou, nalguns casos, o elemento pode fornecer ActualText como alternativa. Só uma leitura da árvore viva consegue distinguir um destes casos de um objecto quebrado que apenas parece aceitável no disco
Ler a árvore de etiquetas viva
A matéria-prima é TPdf.GetStructureElements (também exposta pela propriedade StructureElements), que devolve um TPdfStructureElements, um array plano de registos TPdfStructureElement em ordem do documento. Cada registo é a projecção de um elemento de estrutura vivo com profundidade, tipo, título, texto alternativo e relação pai-filho já resolvidos
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;
O campo TypeName é o ponto em que o validador se apoia. Vem de FPDF_StructElement_GetType, que devolve o tipo de estrutura standard do elemento, o nome /S, depois de o PDFium resolver o role map. AlternateText vem de FPDF_StructElement_GetAltText. Se o PDFium lhe devolver texto alternativo vazio e o elemento for uma Figure, o caso já está encaminhado
O verificador é uma função pura, e isso é intencional
A lógica das regras não vive no método que fala com a DLL. É uma função pública, autónoma e pura:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Recebe um array plano de elementos e devolve um conjunto de problemas. Não chama nenhuma função do PDFium, não abre nenhum documento, não toca em estado global. Essa separação é deliberada, e compensa em dois pontos. Primeiro, testabilidade: pode construir um TPdfStructureElements sintético em memória, alimentá-lo à função e verificar o conjunto de avisos sem qualquer I/O
Segundo, clareza de responsabilidade. TPdf.ValidatePdfUa trata da parte suja - carregar cada página, extrair os seus elementos, agregá-los - e depois entrega um array limpo ao verificador puro. «Obter os dados» (DLL, efeitos secundários, ciclo de vida) e «julgar os dados» (regras, avisos, resultados) ficam em caixas separadas
O que as três regras realmente verificam
A passagem pela árvore de estrutura gera três valores de problema, acrescentados ao fim de TPdfUaValidationIssues para que o enum permaneça ABI-stable para os chamadores existentes: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt e pvuaiNoteMissingId. O corpo é pequeno, mas a ordem é importante porque os consumidores existentes já dependem desses valores
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;
A cláusula 7.3 rege as Figures: um elemento Figure tem de fornecer uma alternativa textual. A primeira versão desta verificação olhava apenas para a entrada Alt, o que a tornava mais restritiva do que os validadores de referência. O PDF/UA aceita uma Figure cujo texto acessível venha de Alt ou de ActualText, por isso a validação tem de aceitar ambas as formas. É um bom exemplo de como uma regra aparentemente simples pode ficar errada quando a implementação assume apenas um caminho
A cláusula 7.9 é diferente na natureza. Uma Note tem de ter um /ID, e esse ID tem de ser único em todo o documento. Um ID em falta é uma falha por elemento. Um ID duplicado é uma relação entre dois elementos, razão pela qual o array plano é importante: para detectar colisões globais, o verificador precisa de ver todos os elementos de uma vez
Agregação por páginas para que a unicidade seja global
O PDFium expõe elementos de estrutura por página, não por documento, por isso a orquestração em ValidatePdfUa tem de os reunir antes de correr as regras. O método percorre cada página com FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, independentemente da página, e concatena tudo num único array
// 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;
A agregação é o que torna correcta a verificação de unicidade da cláusula 7.9. Duas Notes em páginas diferentes podem partilhar um ID; se validasse página a página, nunca veria a colisão, porque o conjunto de elementos de cada página parece internamente consistente. Construir o array completo antes de chamar o verificador é o detalhe que transforma uma verificação local numa verificação global real
Conservador por desenho: falha em silêncio, nunca assinala falso positivo
A propriedade mais importante deste verificador é aquilo que ele se recusa a fazer. Só corresponde aos nomes de tipo /S standard que FPDF_StructElement_GetType devolve directamente - Figure, Formula, Note. Um documento que defina um tipo personalizado e o mapeie através do role map para um dos casos standards fica no lado conservador: se o PDFium lhe apresentar o nome standard, a regra apanha-o; se lhe apresentar outro nome, o verificador não inventa semântica
É também por isso que o âmbito se mantém em três regras. Aninhamento por nível de título (cláusula 7.4), âmbito de cabeçalhos de tabela (7.5) e detecção de ciclos no role map (7.1) são requisitos legítimos de PDF/UA, mas verificá-los bem exige análise de grafo e contexto que um checker de primeira linha não deve fingir que tem. Melhor ser exacto no que cobre do que vasto e impreciso no que promete
A fronteira, dita de forma clara
Há dois limites que convém conhecer antes de integrar isto num gate de release. Primeiro, o verificador só é tão bom quanto aquilo que o PDFium consegue ler do elemento de estrutura. Um punhado de ficheiros de corpus de conformidade que os validadores de referência aceitam usam construção algébrica que o PDFium não expõe totalmente, por isso existem casos de sintaxe legal que ficam fora do alcance do nivel Tier-1
Segundo, isto é um preflight, não uma certificação. O Tier-1 apanha os erros de conteúdo de alta confiança que um scan de bytes estruturalmente não consegue ver, e fá-lo sem falsos alarmes, mas a conformidade completa com PDF/UA, incluindo semântica de headings, âmbito de tabelas e ciclos de role map, continua a precisar de um validador completo. O valor aqui é alinhar o primeiro filtro com o que a árvore de estrutura realmente diz, não substituir todo o ecossistema de conformidade
As APIs da árvore de estrutura e o validador ValidatePdfUa mostrados aqui fazem parte do PDFium Component para Delphi e C++Builder (VCL) e Lazarus/FPC (LCL). A página do produto liga para a referência completa da API, incluindo o TPdfStructureElement inteiro e o conjunto de problemas que o verificador devolve