Um relatório veraPDF que diga que a largura de um glifo discorda do programa de fontes embebido não lhe diz quase nada sobre que glifo é, nem porquê. O PDFlibPas responde a essa questão resolvendo cada código de caráter pelo cmap embebido até um índice de glifo, normalizando a métrica do programa para 1000 unidades por em, e comparando aí
Porque é que as larguras de glifos discordam?
Porque os dois números comparados vivem em sistemas de coordenadas diferentes, e nada no dicionário PDF lhe diz a conversão. Um dicionário de fonte escreve /Widths em espaço de glifo, que o PDF fixa num milésimo do em (ISO 32000-1 §9.2.4). A tabela hmtx dentro do programa TrueType embebido escreve avanços em unidades de desenho da fonte, e a tabela head decide quantas dessas unidades fazem um em: 2048 para a maioria das faces TrueType, 1000 para as derivadas de CFF, ocasionalmente outra coisa completamente diferente. Compare os valores em bruto e toda a fonte de 2048 upem do seu corpus parece avariada. Essa é a armadilha que a ISO 14289-1 §7.21.5 arma para quem tenta auditar larguras lendo campos do dicionário
O PDFlibPas normaliza ao carregar. TPDFTrueTypeParser guarda Advance * 1000 div unitsPerEm no seu array de larguras, pelo que Parser.GetWidth(GID) já responde nos mesmos milésimos de em que o PDF usa, e GetRawWidth continua disponível quando precisa das unidades de desenho de volta. Isso deixa a metade mais difícil: passar de um código de caráter a um índice de glifo. Para uma fonte TrueType simples, o caminho depende da flag Symbolic no FontDescriptor, o bit 3 de /Flags
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// As faces simbólicas são endereçadas diretamente pelo cmap do programa,
// com a convenção de byte alto (3,0) como fallback
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// Não simbólico: código -> nome do 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 pesam. A tolerância é uma unidade, não zero, porque a normalização é divisão inteira e um ficheiro legitimamente produzido pode ficar desviado por uma unidade; é exatamente a redação "dentro de um milésimo de um em" que o diagnóstico 10036 reporta. E a guarda GID < Parser.GlyphCount não é decoração. GetWidth está escrita para ser indulgente com chamadores de renderização, limitando um índice fora do intervalo à última entrada em hmtx e recaindo em 750 quando a tabela está ausente. Indulgente é certo para renderização e errado para auditoria, pelo 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 da mesma forma, com /CIDToGIDMap inserido entre o CID e o glifo. As larguras chegam no array /W, ao qual a ISO 32000-1 §9.7.4.3 dá duas formas que se alternam livremente num só 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 série. A auditoria analisa ambas, depois entrega cada par resultante à mesma comparação, e reporta o total sob o diagnóstico 10037. O passo de mapeamento é onde as fontes compostas diferem, e é por isso que o diagnóstico 10021 de mapa em falta importa antes de ler qualquer largura — um /CIDToGIDMap ausente ou malformado não viola apenas a §7.21.3.2, torna a questão das larguras sem resposta
// /CIDToGIDMap é o nome /Identity ou um stream de índices de 16 bits
// big-endian de glifos, um por CID (secção 9.7.4.2 da ISO 32000-1)
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; // as 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 deve fazer um auditor quando o programa de fontes não descodifica?
Não dizer nada. As verificações de completeza de /CharSet e /CIDSet que a ISO 14289-1 §7.21.4.2 exige — diagnósticos 10038 e 10039 — são o sítio onde um validador demasiado zeloso se torna um passivo, porque um relatório de "o seu CharSet está incompleto" é indistinguível, para quem o lê, de "o nosso descodificador Type 1 desistiu". O PDFlibPas por isso reporta uma entrada em falta apenas quando três coisas têm todas sucesso: o programa de fontes descodifica, o mapeamento de código a glifo resolve, e o próprio conjunto descodifica. TPDFType1Decoder.LoadPFBFromString tem de devolver True e produzir uma contagem de charstrings antes de qualquer nome de glifo ser verificado contra a string /CharSet; o caminho /CIDSet precisa de o stream inflacionar e de a contagem de glifos voltar positiva antes de um único bit ser testado. Qualquer exceção pelo caminho colapsa em "sem achado", e não num defeito
É um enviesamento deliberado para falsos negativos, e vale a pena dizê-lo com clareza em vez de o enterrar. Uma tabela CFF corrompida, uma variante Type 1 não suportada, ou um /CIDSet mais curto do que o intervalo de glifos produzem todos silêncio em vez de um diagnóstico. O raciocínio é que as auditorias PDF/UA são encaminhadas para autores que não construíram a ferramenta, e uma falsa acusação custa mais do que um achado perdido: o autor queima um dia a provar que um ficheiro conforme é conforme, e deixa de confiar em todo o relatório. O Matterhorn Protocol faz a mesma distinção noutra forma quando separa as verificações que uma máquina pode decidir das que exigem um humano, e o seu checkpoint de Fonts (31) é onde estas vivem. Se precisa da leitura mais estrita, corra o PDFlibPas como portão rápido e um validador dedicado como segunda opinião — esse emparelhamento é o mesmo descrito na explicação passo a passo de preflight PDF/A e PDF/UA
O /Contents da página é uma lista, não um stream
O erro mais caro na auditoria de streams de conteúdo é tratar /Contents como um único stream. A ISO 32000-1 §7.7.3.3 permite que uma página contenha um array de streams cuja concatenação, com espaço em branco entre as partes, é o programa da página; os produtores dividem em pontos arbitrários, e um BT pode ficar num membro com o 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 repõe esse estado à entrada. Chame-o uma vez por membro do array e todos os streams depois do primeiro começam sem fonte atual, pelo que texto perfeitamente bem marcado lê-se como ruído sem marcação e sem fonte. O PDFlibPas concatena primeiro e processa uma única 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 por membro
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));
Que Form XObjects contam realmente como não estruturados?
Apenas os que uma página realmente invoca, a partir de um sítio de chamada fora de marked content, e cujo próprio conteúdo mostra texto. O diagnóstico 10040 impõe a ISO 14289-1 §7.20 registando três factos independentes por número de objeto — tem texto, foi invocado, foi invocado dentro de marked content — e reportando apenas a interseção dos dois primeiros menos o terceiro. Cada um dos dois atalhos está errado de uma forma que emitiria: marcar todo o Form com texto em /Resources pune uma biblioteca de templates de que ninguém tira nada, e marcar todo o Form invocado pune logótipos vetoriais que não transportam texto e não precisam de marcação. O sítio de invocação resolve-se por número de objeto e não 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 a §7.21.8, resolvendo cada operando de mostrar texto pela fonte em contexto e contando os códigos que caem em .notdef, o que é proibido independentemente do modo de renderização de texto — incluindo o modo invisível usado atrás de imagens digitalizadas. Como envolver os Forms que sobrevivem é uma questão de árvore de estrutura, está coberto no artigo sobre construir estrutura de PDF marcado
Fontes sem FontDescriptor algum
Uma fonte não incorporada é uma entrada legítima desta auditoria, não um estado de erro, e todos os auxiliares abaixo da verificação de incorporação têm de sobreviver-lhe. Quando o PDFlibPas não encontra /FontDescriptor, ou encontra um descritor sem FontFile, FontFile2 ou FontFile3, regista o diagnóstico 10020 — ou 10022 quando o nome é um dos Standard 14, que a NOTE 5 da §7.21.4 recusa pontualmente isentar — e depois continua pelo resto do ficheiro. Esse é todo o ponto de um relatório: um autor quer todos os achados numa passagem, não um achado por execução. Por isso a referência do descritor entregue aos auxiliares de largura, cmap, CharSet e CIDSet pode ser Nil, e cada um deles testa isso à entrada em vez de assumir que uma verificação anterior abortou a auditoria. Se a correção é incorporar o que falta, a mecânica está na nota sobre incorporar fontes em falta num PDF existente
Executar a auditoria
Uma chamada, sobre um ficheiro que não produziu necessariamente. TPDFlib.CheckFileCompliance recebe um seletor de teste de conformidade — 2 para PDF/UA-1 sob a 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. Os achados de fontes e de streams de conteúdo aqui discutidos ocupam 10020 a 10041 nesse intervalo, mantidos numericamente apartados dos códigos 00xxx de PDF/A para que um registo misto continue legível. Passar 1 em Options termina na primeira ocorrência, o que se quer num portão de build e não numa ferramenta de autoria. Para um documento ainda aberto em memória, GetPDFUADiagnostics corre a inspeção equivalente sem uma volta pelo disco
var
Issues, Count, I: Integer;
begin
// ComplianceTest = 2 seleciona PDF/UA-1; Options = 0 reporta todos os achados
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 disto precisa de um binário validador externo na máquina, o que é a diferença entre uma verificação que corre em cada build e uma verificação que corre quando alguém se lembra. As APIs de conformidade e de diagnóstico aqui descritas enviam-se na PDFlibPas Delphi PDF Library padrão, cuja página de produto traz a tabela completa de códigos de diagnóstico para PDF/UA-1 ao lado das suítes de teste PDF/A, PDF/X e PDF/E