Um relatório do veraPDF dizendo que a largura de um glifo discorda do programa de fonte embutido não diz quase nada sobre qual glifo, ou por quê. O PDFlibPas responde a essa questão resolvendo cada código de caractere pelo cmap embutido até um índice de glifo, normalizando a métrica do programa para 1000 unidades por em e comparando aí
Por que larguras de glifos discordam?
Porque os dois números comparados vivem em sistemas de coordenadas diferentes, e nada no dicionário PDF diz a conversão. Um dicionário de fonte escreve /Widths no espaço de glifo, que o PDF fixa em um milésimo do em (ISO 32000-1 §9.2.4). A tabela hmtx dentro do programa TrueType embutido escreve advances em unidades de design da fonte, e a tabela head decide quantas dessas unidades fazem um em: 2048 para a maioria das fontes TrueType, 1000 para as derivadas de CFF, ocasionalmente outra coisa completamente diferente. Compare os valores brutos e toda fonte de 2048 upem do seu corpus parece quebrada. Essa é a armadilha que o ISO 14289-1 §7.21.5 arma para quem tenta auditar larguras lendo campos do dicionário
O PDFlibPas normaliza no load. TPDFTrueTypeParser guarda Advance * 1000 div unitsPerEm em seu array de larguras, de modo que Parser.GetWidth(GID) já responde nos mesmos milésimos de em que o PDF usa, e GetRawWidth continua disponível quando você precisa das unidades de design de volta. Isso ainda deixa a metade mais difícil: ir de um código de caractere a um índice de glifo. Para uma fonte TrueType simples, a rota depende da flag Symbolic no FontDescriptor, o bit 3 de /Flags
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// Fontes simbólicas são endereçadas pelo cmap do programa diretamente,
// com a convenção (3,0) de byte alto como fallback
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// Não simbólica: code -> nome de glifo via encoding, nome -> Unicode
// via Adobe Glyph List, Unicode -> GID via o cmap do programa
UnicodeValue := GetGlyphUnicode(EncodingNames[Code and $FF]);
if UnicodeValue = 0 then
Continue;
GID := Parser.GetGlyphIndex(UnicodeValue);
end;
if (GID > 0) and (GID < Parser.GlyphCount) then
if Abs(PDFWidth - Parser.GetWidth(GID)) > 1 then
Inc(MismatchCount);
finally
Parser.Free;
end;
Dois detalhes nesse fragmento têm peso. A tolerância é de uma unidade, não zero, porque a normalização é divisão inteira e um arquivo produzido legitimamente pode sair uma unidade fora; esse é exatamente o texto "dentro de um milésimo de um em" que o diagnóstico 10036 relata. E a guarda GID < Parser.GlyphCount não é decoração. GetWidth é escrita para ser tolerante com chamadores de renderização, fixando um índice fora da faixa na última entrada de hmtx e recaindo para 750 quando a tabela está ausente. Tolerante é certo para renderização e errado para auditoria, de modo que a auditoria rejeita o índice antes de pedir uma largura em vez de confiar no clamp
CIDFontType2 acrescenta mais uma indireção
O PDFlibPas percorre fontes compostas do mesmo modo, com /CIDToGIDMap inserida entre o CID e o glifo. As larguras chegam no array /W, ao qual o ISO 32000-1 §9.7.4.3 dá duas formas que se alternam livremente em um único array: um CID inicial seguido de um array de larguras consecutivas, ou um primeiro CID, um último CID e uma única largura aplicada a toda a sequência. A auditoria analisa ambas, entrega cada par resultante à mesma comparação e relata o total sob o diagnóstico 10037. O passo de mapeamento é onde fontes compostas diferem, e é por isso que o diagnóstico 10021 de mapa ausente importa antes de você ler qualquer largura — um /CIDToGIDMap ausente ou malformado não apenas viola o §7.21.3.2, ele torna a pergunta da largura sem resposta
// /CIDToGIDMap é o nome /Identity ou um stream de índices de glifo
// 16-bit big-endian, um por CID (ISO 32000-1 seção 9.7.4.2)
Obj := DerefIndRef(FDoc, CIDFont.FindValueByKeyName('CIDToGIDMap'));
if (Obj is TPDFName) and (TPDFName(Obj).Name = 'Identity') then
begin
GID := CID;
Result := True;
end
else if Obj is TPDFStream then
begin
Data := TPDFStream(Obj).GetDecodedStream;
P := CID * 2 + 1; // strings Pascal são de base 1
if (P >= 1) and (P + 1 <= Length(Data)) then
begin
GID := (Integer(Byte(Data[P])) shl 8) or Integer(Byte(Data[P + 1]));
Result := True;
end;
end;
O que um auditor deve fazer quando o programa de fonte não decodifica?
Não dizer nada. As verificações de completude de /CharSet e /CIDSet que o ISO 14289-1 §7.21.4.2 exige — diagnósticos 10038 e 10039 — são o lugar onde um validator ansioso demais se torna um passivo, porque um relato de "seu CharSet está incompleto" é indistinguível, para quem o lê, de "nosso decodificador Type 1 desistiu". O PDFlibPas portanto relata uma entrada ausente só quando três coisas têm sucesso: o programa de fonte decodifica, o mapeamento de código a glifo se resolve e o próprio conjunto decodifica. TPDFType1Decoder.LoadPFBFromString precisa devolver True e produzir uma contagem de charstrings antes que qualquer nome de glifo seja conferido contra a string /CharSet; o caminho de /CIDSet precisa que o stream infle e que a contagem de glifos volte positiva antes que um único bit seja testado. Qualquer exceção no caminho colapsa em "sem constatação", não em um defeito
Esse é um viés deliberado para falsos negativos, e vale declará-lo com franqueza em vez de escondê-lo. Uma tabela CFF corrompida, uma variante Type 1 não suportada ou um /CIDSet mais curto que a faixa de glifos produzem silêncio em vez de um diagnóstico. O raciocínio é que auditorias PDF/UA são encaminhadas a autores que não construíram as ferramentas, e uma acusação falsa custa mais do que uma constatação perdida: o autor queima um dia provando que um arquivo em conformidade está em conformidade, e para de confiar no relatório inteiro. O Matterhorn Protocol faz a mesma distinção em outra forma quando separa verificações que uma máquina decide das que um humano precisa, e seu checkpoint Fonts (31) é onde estas vivem. Se você precisa da leitura mais estrita, rode o PDFlibPas como o portão rápido e um validator dedicado como segunda opinião — esse pareamento é o mesmo descrito no passo a passo de preflight PDF/A e PDF/UA
Page /Contents é uma lista, não um stream
O único erro mais caro na auditoria de content streams é tratar /Contents como um único stream. O ISO 32000-1 §7.7.3.3 permite que uma página guarde um array de streams cuja concatenação, com espaços em branco entre as partes, é o programa da página; produtores dividem em pontos arbitrários, e um BT pode ficar em um membro com seu ET correspondente no seguinte. Um processador de conteúdo mantém estado — a profundidade de aninhamento de marked-content, a fonte selecionada pelo último Tf, a flag de objeto de texto — e Process reinicia esse estado ao entrar. Chame-o uma vez por membro do array e todo stream depois do primeiro começa sem fonte corrente, de modo que texto perfeitamente marcado lê como ruído sem marca e sem fonte. O PDFlibPas concatena primeiro e processa uma vez
function ContentObjectData(FDoc: TSmartPDFDocument; Obj: TPDFObject): AnsiString;
var
I: Integer;
begin
Result := '';
Obj := DerefIndRef(FDoc, Obj);
if Obj is TPDFStream then
Result := TPDFStream(Obj).GetDecodedStream
else if Obj is TPDFArray then
for I := 0 to TPDFArray(Obj).Count - 1 do
Result := Result + ContentObjectData(FDoc, TPDFArray(Obj).Item[I]) + #10;
end;
// Uma chamada Process sobre toda a concatenação, nunca uma chamada por membro
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));
Quais Form XObjects realmente contam como não estruturados?
Apenas os que uma página realmente invoca, de um ponto de chamada fora de marked content, cujo próprio conteúdo mostra texto. O diagnóstico 10040 aplica o ISO 14289-1 §7.20 registrando três fatos independentes por número de objeto — tem texto, foi invocado, foi invocado dentro de marked content — e relatando apenas a interseção dos dois primeiros menos o terceiro. Cada um dos dois atalhos está errado de um modo que você embarcaria: sinalizar todo Form com texto em /Resources pune uma biblioteca de templates de que ninguém puxa, e sinalizar todo Form invocado pune logotipos vetoriais que não carregam texto e não precisam de marcação. O sítio de invocação é resolvido por número de objeto em vez de por nome de recurso, já que o mesmo Form rotineiramente é alcançado por nomes diferentes em páginas diferentes. O diagnóstico companheiro 10041 percorre o mesmo programa concatenado para o §7.21.8, resolvendo cada operando que mostra texto pela fonte em escopo e contando os códigos que caem em .notdef, o que é proibido independentemente do modo de renderização de texto — inclusive o modo invisível usado atrás de imagens digitalizadas. Como os Forms sobreviventes devem ser envolvidos é uma pergunta de árvore de estrutura, coberta no artigo sobre construir estrutura PDF tagged
Fontes sem nenhum FontDescriptor
Uma fonte não embutida é uma entrada legítima desta auditoria, não um estado de erro, e todo auxiliar abaixo da verificação de embutimento precisa sobreviver a ela. Quando o PDFlibPas não encontra /FontDescriptor, ou encontra um descritor sem FontFile, FontFile2 ou FontFile3, ele registra o diagnóstico 10020 — ou 10022 quando o nome é um dos Standard 14, que o §7.21.4 NOTE 5 recusa veementemente isentar — e então continua pelo resto do arquivo. Esse é todo o ponto de um relatório: um autor quer toda constatação em uma passada, não uma constatação por execução. Assim, a referência de descritor entregue aos auxiliares de largura, cmap, CharSet e CIDSet pode ser Nil, e cada um deles testa isso na entrada em vez de assumir que uma verificação anterior abortou a auditoria. Se o conserto é embutir o que falta, a mecânica está na nota sobre embutir fontes ausentes em um PDF existente
Executando a auditoria
Uma chamada, sobre um arquivo que você não necessariamente produziu. TPDFlib.CheckFileCompliance recebe um seletor de teste de conformidade — 2 para PDF/UA-1 sob o ISO 14289-1:2014 — e devolve zero ou um handle de lista de strings cujas entradas são um código numérico, dois pontos e uma mensagem legível. As constatações de fonte e de content stream discutidas aqui ocupam 10020 a 10041 nessa faixa, mantidas numericamente separadas dos códigos 00xxx do PDF/A para que um log misto continue legível. Passar 1 em Options faz curto-circuito na primeira constatação, que é o que você quer em um portão de build, não em uma ferramenta de autoria. Para um documento ainda aberto em memória, GetPDFUADiagnostics executa a inspeção equivalente sem uma volta pelo disco
var
Issues, Count, I: Integer;
begin
// ComplianceTest = 2 seleciona PDF/UA-1; Options = 0 relata toda constatação
Issues := PDF.CheckFileCompliance('delivery.pdf', '', 2, 0);
if Issues = 0 then
WriteLn('delivery.pdf: PDF/UA-1 conformant')
else
begin
Count := PDF.GetStringListCount(Issues);
for I := 1 to Count do
WriteLn(' ', PDF.GetStringListItem(Issues, I)); // ex.: 10037 CIDFontType2 ...
end;
end;
Nada disso precisa de um binário validator externo na máquina, o que é a diferença entre uma verificação que roda a cada build e uma que roda quando alguém lembra. As APIs de conformidade e diagnóstico descritas aqui vêm na PDFlibPas Delphi PDF Library padrão, cuja página de produto carrega a tabela completa de códigos de diagnóstico do PDF/UA-1 junto com as suítes de teste PDF/A, PDF/X e PDF/E