Artigo Técnico

Lidar com PDFs com Referência Híbrida (Hybrid-Reference) de Aplicações Office em Delphi

Ao exportar um documento do Microsoft Word ou do Excel com Guardar como PDF, o ficheiro resultante em disco é, na maioria das vezes, um ficheiro de referência híbrida. Este transporta a sua informação de referência cruzada duas vezes: uma como a tabela clássica de largura fixa que terminava todos os PDF até à versão 1.4, e outra como um fluxo de referência cruzada comprimido do qual depende, na prática, a maior parte do documento. Uma única chave do trailer, /XRefStm, une as duas vistas, e o facto de uma ferramenta ver ou não o documento completo depende de esta seguir essa chave

Este artigo analisa os ficheiros híbridos do lado do consumo: o aspeto dos bytes no final do ficheiro, o modo como as duas vistas se desalinham ao longo de edições, e o modo como um pipeline em Delphi pode detetar e encaminhar entradas híbridas. O modo como um carregador funde as duas vistas, e a razão pela qual a ordem não é negociável, é o tema do nosso artigo do HotPDF sobre o carregamento de ficheiros de referência híbrida; este artigo trata, antes de mais, de reconhecer o esquema

Porque as exportações do Office escrevem o índice duas vezes

O PDF 1.5 introduziu duas funcionalidades que alteraram a forma do ficheiro: os fluxos de referência cruzada, que guardam o índice de objetos como dados binários comprimidos em vez de uma tabela em texto simples, e os fluxos de objetos, que agrupam muitos objetos pequenos num único contentor comprimido com Flate. Um escritor que os utilize produz ficheiros mais pequenos, mas um leitor PDF 1.4 não consegue abrir o resultado, porque as estruturas em que se baseia, a palavra-chave xref e o dicionário trailer, deixaram de existir

A norma ISO 32000-1 §7.5.8.4 define o compromisso. Um ficheiro de referência híbrida escreve ambas as formas: uma tabela de referência cruzada clássica que endereça os objetos que um leitor antigo tem de alcançar, entre eles o catálogo e a árvore de páginas, e um fluxo de referência cruzada que indexa tudo o resto. Os objetos incluídos em fluxos de objetos são marcados como livres na tabela clássica, pelo que um leitor 1.4 os ignora sem qualquer problema; as suas localizações reais só existem no fluxo. O trailer clássico transporta então uma chave /XRefStm com o deslocamento em bytes desse fluxo. Um visualizador antigo nunca lê essa chave e apresenta o ficheiro a partir da vista da tabela. Um visualizador moderno segue-a e vê o documento completo. O Word e o Excel têm produzido exatamente este esquema há anos, razão pela qual os ficheiros híbridos não são um caso de nicho exótico, mas sim uma grande parte do que os pipelines empresariais recebem

PDF: Cauda de um PDF de referências híbridas em que um leitor legado confia na tabela xref clássica, enquanto um leitor moderno segue o /XRefStm até à vista de stream comprimida
Uma única chave de trailer decide a vista: a tabela clássica serve os leitores antigos, enquanto /XRefStm entrega aos leitores modernos o fluxo que localiza todo o resto

O aspeto da cauda de um ficheiro híbrido

O esquema é mais fácil de compreender a partir dos bytes. Segue-se a cauda de um pequeno ficheiro híbrido, com os deslocamentos abreviados; numa exportação real do Office, o valor de /XRefStm é tipicamente um deslocamento grande, próximo do final do ficheiro. A ordem de leitura é o percurso a começar pela cauda, descrito na nossa visão geral da estrutura de ficheiros PDF: encontrar %%EOF, ler startxref, saltar para a tabela

% ... objetos do corpo, incluindo object streams e, no byte 116,
% a cross-reference stream (um objeto stream com /Type /XRef) ...

xref                    % secção classic: para onde startxref aponta
0 4
0000000000 65535 f      % slot 0: cabeça da free list, sempre presente
0000000017 00000 n      % objeto 1: o catalog, visível a qualquer leitor
0000000000 65535 f      % objeto 2: marcado como free -- vive numa object stream
0000000000 65535 f      % objeto 3: idem; só a vista da stream o localiza
trailer
<<
  /Size 4
  /Root 1 0 R
  /XRefStm 116          % byte offset da cross-reference stream
>>
startxref
7164                    % byte offset da keyword 'xref' acima
%%EOF

Dois pormenores neste excerto sustentam todo o mecanismo. Primeiro, o startxref aponta propositadamente para a secção clássica: esse é o endereço onde um leitor antigo tem de aterrar. O fluxo de referência cruzada só é alcançável através da chave /XRefStm dentro do dicionário trailer, pelo que um analisador que nunca procure essa chave nunca chega a saber que o fluxo existe. Segundo, os objetos 2 e 3 são mentiras de um tipo benigno. A tabela clássica declara-os como livres, mas são objetos reais dentro de um contentor comprimido; essa marcação como livre é o que impede um leitor 1.4 de tropeçar em entradas que não consegue utilizar. Um consumidor que confie apenas na vista clássica conclui que a maior parte deste documento não existe

Como as duas vistas se desalinham

Um ficheiro híbrido recém-saído do Word é internamente consistente: ambas as vistas descrevem o mesmo documento, cada uma dentro do seu âmbito declarado. O problema começa quando o ficheiro é editado por uma ferramenta que só compreende uma das vistas. Considere-se um utilitário de carimbagem que acrescenta uma atualização incremental de estilo clássico: novos objetos, uma nova secção xref, uma cadeia /Prev até à secção anterior, e um novo trailer. Se esse trailer descartar a chave /XRefStm, a vista do fluxo fica órfã; se transportar o valor antigo para a frente, a vista do fluxo continua a descrever o documento tal como estava antes da edição. De qualquer das formas, os dois índices passam a discordar sobre o conteúdo do ficheiro

O ficheiro resultante tem uma assinatura de falha distintiva: objetos visíveis numa vista estão em falta ou desatualizados na outra. Um leitor que resolve através da vista do fluxo encontra a versão anterior à edição de um objeto atualizado, ou nenhuma entrada para um objeto acrescentado. Um leitor que use a vista da tabela vê a edição, mas perde o rasto aos objetos comprimidos que só o fluxo localiza. Na prática, isto manifesta-se como campos de formulário que sobrevivem num visualizador e desaparecem noutro, anotações que uma passagem de carimbagem parece ter eliminado, ou pesquisas que acabam a apontar para o objeto errado

O que torna estes ficheiros dispendiosos de depurar é o facto de o Adobe Acrobat normalmente os abrir sem qualquer problema: quando o índice discorda dos bytes, reconstrói silenciosamente os dados de referência cruzada procurando cabeçalhos de objeto, pelo que quem produziu o ficheiro danificado não vê nada de errado. A falha só surge mais tarde, quando o ficheiro chega a um consumidor rigoroso, um validador de preflight, um serviço de assinatura, uma tarefa de ingestão para arquivo, que confia na estrutura declarada e reporta objetos em falta ou uma incompatibilidade de referência cruzada. "Abre bem no Acrobat" é como começa quase todos os pedidos de suporte sobre dessincronização híbrida

Detetar um ficheiro híbrido em Delphi simples

Classificar as entradas não requer uma biblioteca PDF. A chave /XRefStm só pode ocorrer dentro de um dicionário trailer clássico, e o trailer ativo situa-se dentro dos últimos kilobytes do ficheiro, porque a especificação exige que %%EOF apareça perto do final físico. Ler uma janela limitada no final do ficheiro e pesquisá-la é suficiente para a triagem:

uses
  System.SysUtils, System.Classes, System.StrUtils, System.Math;

function IsHybridReferencePdf(const FileName: string): Boolean;
const
  TailWindow = 2048;
var
  Stream: TFileStream;
  Buf: TBytes;
  Tail: string;
  Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
  Result := False;
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    if Stream.Size < 48 then
      Exit;
    Len := Min(TailWindow, Integer(Stream.Size));
    SetLength(Buf, Len);
    Stream.Position := Stream.Size - Len;
    Stream.ReadBuffer(Buf[0], Len);
  finally
    Stream.Free;
  end;

  // Todas as palavras-chave envolvidas são ASCII de 7 bits, pelo que uma descodificação byte a byte é segura
  Tail := TEncoding.ANSI.GetString(Buf);

  // Encontrar a ÚLTIMA palavra-chave 'trailer': com atualizações incrementais,
  // o trailer mais recente é o que rege o ficheiro
  TrailerPos := 0;
  NextPos := Pos('trailer', Tail);
  while NextPos > 0 do
  begin
    TrailerPos := NextPos;
    NextPos := PosEx('trailer', Tail, NextPos + 1);
  end;
  if TrailerPos = 0 then
    Exit;  // sem trailer clássico: um ficheiro puro de fluxo xref, não híbrido

  // Um trailer híbrido transporta /XRefStm entre 'trailer' e 'startxref'
  KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
  StartXrefPos := PosEx('startxref', Tail, TrailerPos);
  Result := (KeyPos > 0) and
    ((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;

Os três resultados correspondem aos três esquemas. Um ficheiro apenas clássico tem um trailer mas não tem /XRefStm: False. Um ficheiro que aposta totalmente em fluxos de referência cruzada não tem qualquer palavra-chave trailer, as suas chaves de trailer residem no dicionário do fluxo: também False, corretamente, porque esse ficheiro está comprimido, não é híbrido. Só o esquema de indexação dupla devolve True

Fluxo de decisão Delphi a varrer a cauda do ficheiro à procura do último trailer e do /XRefStm e, depois, a encaminhar PDFs híbridos para validação, normalização ou tratamento de apenas anexar
Uma pesquisa de cauda limitada produz três vereditos, e só o caso de duplo índice segue em frente como híbrido verdadeiro

Para uso em produção, valem a pena duas linhas extra de robustez. Analisar o número inteiro a seguir a /XRefStm, posicionar-se nesse deslocamento e confirmar que um objeto de fluxo com /Type /XRef está realmente lá; um ficheiro truncado pode transportar a chave enquanto o fluxo desapareceu, o que pertence a uma categoria diferente de um híbrido saudável. E tratar o tamanho da janela como um parâmetro: 2 KB cobre a saída habitual do Office, mas um dicionário trailer invulgarmente grande pode empurrar a palavra-chave para fora do intervalo, e alargar a janela é preferível a declarar o ficheiro como clássico por engano

Encaminhar ficheiros híbridos através de um pipeline em Delphi

A deteção proporciona uma decisão de encaminhamento. Para ficheiros que são apenas lidos, renderizados ou validados, deve usar-se um carregador que resolva ambas as vistas, e depois verificar o comportamento em vez dos bytes. O PDFium Component analisa a cadeia /XRefStm durante o carregamento, pelo que a tabela de objetos que o código vê já é a fundida, e as verificações descritas no nosso artigo sobre a validação de fluxos de objetos e de referência cruzada aplicam-se sem alterações. Se um híbrido dessincronizado estiver danificado ao ponto de recusar o carregamento, o motor reporta-o através do seu conjunto de erros, FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY e FPDF_ERR_PAGE, sendo FPDF_ERR_FORMAT o que os danos estruturais produzem. No entanto, não convém apoiar-se nesse sinal: o PDFium é permissivo por design e reconstrói silenciosamente a maioria dos ficheiros inconsistentes, pelo que um carregamento bem-sucedido prova que o ficheiro era recuperável, não que as suas duas vistas concordam. A verificação de consistência significativa é comparar o que um percurso completo pelos objetos encontra com o que o /Size do trailer declara

Para ficheiros que o pipeline modifica, a política mais segura é deixarem simplesmente de ser híbridos. Um carregamento seguido de uma gravação completa através do HotPDF reescreve o documento com uma única referência cruzada, autoconsistente e numa só forma: sem /XRefStm, sem uma segunda vista que possa dessincronizar-se, cada objeto pertencente a exatamente uma entrada de índice. Essa normalização é o que convém fazer antes da ingestão para arquivo, antes de um RIP downstream rigoroso ou de um serviço de assinatura, e depois de qualquer edição aplicada a uma entrada híbrida. Funciona porque o carregador fundiu corretamente as vistas à entrada, o mecanismo que o artigo do HotPDF sobre referência híbrida percorre em detalhe

A única classe de ficheiros a não tocar são os documentos assinados digitalmente. Uma reescrita completa desloca todos os bytes, o que invalida qualquer assinatura calculada sobre os intervalos originais. Uma alteração a um híbrido assinado tem de entrar como uma atualização incremental adequada que mantenha ambas as vistas; um ficheiro que só precise de ser lido deve passar sem qualquer alteração. A normalização é para ficheiros próprios; a ficheiros assinados, apenas se deve sempre acrescentar

Os PDF de referência híbrida não estão malformados; são a própria ponte de compatibilidade do formato, e as aplicações do Office vão continuar a produzi-los enquanto sobreviverem leitores PDF 1.4 na base instalada. Um pipeline capaz de detetar a chave /XRefStm, validar o documento fundido com o PDFium Component, e regenerar uma saída limpa de índice único com o HotPDF Delphi Component trata-os pelo que realmente são: entradas comuns com um sinal extra no trailer