O HotPDF pode guardar um documento carregado numa forma de diagnóstico concebida para ser lida e editada por uma pessoa, e depois reparar o ficheiro depois de o ter editado. SaveLoadedDocumentDiagnostic escreve o documento com uma ordenação determinística de dicionários, cada objeto como um objeto indireto normal, sem object streams, sem cross-reference stream e sem linearização. RepairDiagnosticDocument reconstrói depois os offsets de bytes, os comprimentos de stream, a tabela de referências cruzadas e o trailer que a sua edição invalidou
Este par existe para um único fluxo de trabalho: compreender ou corrigir o que está realmente dentro de um PDF. Todos os programadores que já depuraram um problema de renderização já quiseram abrir o ficheiro, alterar um número num dicionário e ver o resultado. Num PDF normal isso é impossível, porque o conteúdo está comprimido em object streams e cada byte que insere desloca offsets que uma tabela de referências cruzadas regista com exatidão
O que torna diferente uma gravação de diagnóstico?
Seis definições mudam em conjunto. As chaves dos dicionários são escritas por ordem determinística, pelo que duas gravações do mesmo documento produzem ficheiros comparáveis. O modo de saída de diagnóstico anota a estrutura. A reescrita completa é forçada, pelo que nada é transportado como uma revisão incremental intocada. A linearização está desativada, uma vez que um ficheiro linearizado está organizado para um visualizador web, e não para um leitor. As cross-reference streams estão desativadas, pelo que a tabela é texto simples. E os object streams estão desativados, pelo que nenhum objeto fica escondido dentro de um contentor comprimido
O resultado é um ficheiro cujo cabeçalho traz um marcador de diagnóstico junto da assinatura %PDF- habitual, e cujo corpo pode percorrer em qualquer editor. Os objetos surgem uns a seguir aos outros, os dicionários são legíveis e os content streams ficam onde os consegue encontrar. A ferramenta de reparação verifica esse marcador e recusa ficheiros que não o tenham, porque a estrutura compacta de um PDF normal não é algo que um reparador por intervalos de bytes deva tentar interpretar
uses
HPDFDoc;
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice.pdf') <= 0 then
Exit;
// Legível, um objeto por bloco, nada escondido em object streams
Pdf.SaveLoadedDocumentDiagnostic('invoice-diagnostic.pdf');
finally
Pdf.Free;
end;
end;
Porque é que editar quebra o ficheiro, e o que tem de ser reconstruído?
Três elementos num PDF codificam posições ou contagens de bytes. A tabela de referências cruzadas mapeia cada número de objeto para um offset de bytes. O valor startxref no final do ficheiro aponta para essa tabela. E cada dicionário de stream traz um /Length que indica quantos bytes ocupa o corpo da stream
Altere um carácter em qualquer ponto antes do final do ficheiro e os offsets a seguir a esse ponto ficam errados. Acrescente uma linha a um content stream e o respetivo /Length também fica errado. A maioria dos visualizadores recusa o ficheiro, e os que o recuperam fazem-no por varrimento, o que esconde o erro em vez de o corrigir. A reparação reconstrói os três elementos a partir do que os bytes efetivamente dizem agora
var
Options: THPDFDiagnosticRepairOptions;
Report: THPDFDiagnosticRepairReport;
begin
Options := THPDFDiagnosticRepairOptions.Default;
Options.VerifyOutput := True; // carregar o resultado antes de substituir
if THotPDF.RepairDiagnosticDocument('invoice-diagnostic.pdf',
'invoice-fixed.pdf', Options, Report) then
Writeln(Format('%d objects, %d streams, %d lengths corrected, ' +
'%d xref rows rebuilt, %d pages verified',
[Report.ObjectCount, Report.StreamCount,
Report.RepairedStreamLengthCount, Report.RebuiltXRefEntryCount,
Report.VerifiedPageCount]))
else
Writeln(Format('repair failed (%d): %s',
[Ord(Report.Status), string(Report.Diagnostic)]));
end;
Copiar bytes, não reserializar
A decisão de conceção que torna esta ferramenta fiável é o facto de nunca carregar o documento e voltar a escrevê-lo. Ela copia os intervalos de bytes dos objetos que validou, altera apenas os valores de /Length que estão errados, e reconstrói à volta deles a tabela, o trailer e o startxref
Carregar e reserializar seria muito mais fácil de implementar e alteraria coisas silenciosamente. A sintaxe de objetos desconhecida seria normalizada, os escapes de strings seriam reescritos na forma preferida do escritor, e as cargas úteis de stream já codificadas poderiam ser recodificadas. Para uma ferramenta de depuração isso é inaceitável: editou um número, e o diff deveria mostrar um número, não mil alterações de formatação incidentais. Se quiser saber exatamente o que mudou entre dois ficheiros, isso é um problema de comparação, abordado em comparar dois ficheiros PDF por estrutura e por pixels
As duas regras de análise que evitam objetos falsos
Um analisador por intervalos de bytes enfrenta uma armadilha óbvia: texto que se parece com um cabeçalho de objeto pode surgir dentro da carga útil de uma stream ou dentro de uma string. Um varrimento ingénuo emitiria uma linha de referência cruzada a apontar para o meio de um content stream, e o ficheiro resultante ficaria danificado de uma forma nova e mais confusa
O HotPDF evita isto saltando todo o intervalo de bytes de cada objeto validado, usando o seu offset final registado, pelo que nada dentro do corpo de um objeto é alguma vez considerado candidato. Entre objetos, só são permitidos espaços em branco PDF e comentários; qualquer outra coisa significa que a região não é compreendida e os seus bytes não devem ser incluídos na saída
Os comprimentos de stream exigem o mesmo cuidado. O comprimento real da carga útil exclui o delimitador de fim de linha que um escritor insere antes de endstream. Quando o comprimento declarado cai exatamente sobre o marcador, essa declaração é aceite. Quando não cai, o reparador efetua uma pesquisa limitada pelo par endstream e endobj e subtrai exatamente um delimitador, um CR, um LF ou um CRLF. Errar isto por um único byte produz um ficheiro que abre e renderiza uma página com um operador perdido no final do seu content stream
O que o trailer tem permissão para manter
O trailer reconstruído não é uma cópia do antigo. /Size, /Prev e /XRefStm descrevem todos a disposição anterior do ficheiro e são recalculados ou eliminados. Só sobrevivem as entradas cujos alvos foram validados: /Info, /ID e /Encrypt. E /Root tem de resolver para exatamente um objeto Catalog — nem zero, nem vários, que é o que tende a resultar de um ficheiro montado por copiar e colar blocos de objetos
A segurança da saída é tratada da forma que deve ser numa ferramenta que atua sobre ficheiros que importam às pessoas. O resultado é escrito num ficheiro temporário no diretório de destino, depois verificado carregando-o pelo percurso normal com a recuperação de referências cruzadas desativada, pelo que um ficheiro que só abre graças à recuperação conta como uma falha. Só depois de a árvore de páginas ser verificada é que o ficheiro temporário substitui atomicamente o destino. Uma reparação falhada nunca substitui uma saída já existente
Limites e códigos de falha honestos
Três limites delimitam o trabalho: bytes de entrada, número de objetos candidatos e número máximo de objeto, que assumem por predefinição 8 GiB, dois milhões e dois milhões. O limite do número de objeto importa mais do que parece, porque uma tabela esparsa com alguns objetos numerados na casa dos milhares de milhões inflacionaria de outro modo enormemente a tabela de referências cruzadas reconstruída. As tabelas de referências cruzadas clássicas também rejeitam offsets que precisassem de mais de dez dígitos decimais, o que é um limite do formato e não uma opção de implementação
As falhas reportam um estado específico em vez de um booleano: argumentos inválidos, origem demasiado grande, não é um ficheiro de diagnóstico, limite de objetos excedido, objeto inválido, objeto duplicado, catálogo em falta, trailer inválido, erro de saída, ou validação falhada. Cada um corresponde a uma correção diferente. Um objeto duplicado significa que colou um bloco de objeto duas vezes. Um catálogo em falta significa normalmente que o objeto que contém /Type /Catalog foi danificado. Validação falhada significa que a estrutura já é consistente, mas a árvore de páginas não carrega, o que tipicamente é um array /Kids editado de forma dessincronizada com o seu /Count
Duas notas práticas fecham o ciclo. Guarde o ficheiro de diagnóstico, não apenas o reparado, para que a próxima edição parta de uma base legível. E não envie a saída de diagnóstico a utilizadores finais: é maior do que uma gravação normal, porque nada está comprimido em object streams, e o percurso de produção para um ficheiro compacto é o percurso de gravação habitual descrito em object streams e atualizações incrementais. Quando o objetivo é encontrar o que está errado, em vez de o alterar, o percurso automatizado em automação de relatórios de pré-impressão (preflight) responde mais depressa do que ler bytes
A gravação de diagnóstico, a reparação, o preflight e a comparação operam todos sobre o mesmo modelo de documento carregado em Delphi e C++Builder; a lista completa de funcionalidades está disponível na página do componente PDF HotPDF para Delphi