Technical Article

Erros de Verificação de Intervalo em Bibliotecas PDF em Delphi: Causas Raiz

Os erros de verificação de intervalo (range check errors) em bibliotecas PDF em Delphi têm a reputação de ser difíceis de identificar porque não seguem um padrão de entrada consistente. O mesmo documento provoca-os num computador e noutro não; o mesmo caminho de código dispara a exceção num ficheiro de 3 páginas mas executa sem problemas num de 12 páginas. Essa inconsistência deve-se quase sempre a uma única causa raiz: os objetos de página PDF não são armazenados na ordem do ficheiro. Se a biblioteca construir a sua matriz de páginas interna analisando os objetos sequencialmente, em vez de percorrer a árvore de páginas declarada pelo catálogo, constrói um índice cujo intervalo válido não corresponde ao que os invocadores esperam, e a verificação de intervalo deteta essa discrepância no pior momento possível.

Como funciona a verificação de intervalo no Delphi

Com a diretiva de compilação {$R+} ativa (o padrão na configuração Debug), a RTL do Delphi valida cada índice de matriz, índice de string e atribuição enumerada em tempo de execução. Um acesso fora dos limites gera uma exceção ERangeError em vez de ler silenciosamente a memória adjacente. Esse comportamento é valioso: expõe erros latentes cedo, em vez de permitir que corrompam uma estrutura de dados que só falhará cem linhas depois. A parte frustrante é que a exceção é disparada no local de acesso, não no ponto em que o índice foi calculado incorretamente. Quando a pilha de chamadas (call stack) mostra um método profundamente aninhado numa unidade de PDF, o erro real costuma estar vários frames atrás.

As condições booleanas compostas agravam este cenário. O Delphi avalia expressões and da esquerda para a direita com semântica de curto-circuito, mas o curto-circuito apenas ignora a avaliação quando o lado esquerdo é False. Uma expressão como:

if FDocStarted and (DestIndex < Length(PageArr)) and
   (PageArr[DestIndex].PageObj <> nil) then

parece segura, mas só protege contra um índice fora do intervalo se FDocStarted for True e DestIndex for não negativo. A verificação DestIndex < Length(PageArr) não faz nada quando DestIndex for negativo, porque comparar um número inteiro negativo com um comprimento não negativo devolve True em aritmética com sinal, e o acesso subsequente à matriz continua a disparar o erro de intervalo. Mover a verificação de limites para a posição mais externa é a correção correta:

if (DestIndex >= 0) and (DestIndex < Length(PageArr)) then
begin
  if FDocStarted and (PageArr[DestIndex].PageObj <> nil) then
    Result := PageArr[DestIndex].PageObj
  else
    Result := nil;
end
else
  raise ERangeError.CreateFmt(
    'Page index %d is out of range (0..%d)',
    [DestIndex, Length(PageArr) - 1]);

Esta é a correção mecânica. Evita o crash. Não explica por que razão DestIndex recebeu um valor fora do intervalo válido.

A causa real: ordem dos objetos versus ordem das páginas

A norma ISO 32000-1 §7.7.3 define a árvore de páginas como uma árvore de nós Pages cujas matrizes Kids listam objetos de página na ordem de exibição. O ficheiro armazena esses objetos nos desvios que o escritor escolheu; o objeto número 20 pode preceder fisicamente o objeto número 3 no fluxo de bytes. Uma biblioteca que construa a sua lista de páginas iterando a tabela de referências cruzadas na ordem do número do objeto, em vez de seguir a cadeia Kids, produzirá uma sequência que diverge do que o utilizador espera. Em documentos onde o gerador escreveu as páginas em ordem, tudo funciona. Em documentos onde não o fez, a discrepância entre a numeração de páginas da biblioteca e a do invocador produz índices que ficam fora de PageArr.

A abordagem correta consiste em começar pelo catálogo, resolver a referência indireta /Pages e percorrer a matriz Kids de forma recursiva. Para um documento plano sem nós Pages intermédios, a travessia é direta:

procedure BuildPageIndexFromTree(
  const KidsArray: THPDFArray;
  var PageArr: TPageObjArray);
var
  i, Idx: Integer;
  Child: THPDFObject;
  ChildType: string;
begin
  for i := 0 to KidsArray.Count - 1 do
  begin
    Child := KidsArray.GetIndirectObject(i);
    if Child = nil then
      Continue;
    ChildType := Child.GetNameValue('/Type');
    if ChildType = 'Page' then
    begin
      Idx := Length(PageArr);
      SetLength(PageArr, Idx + 1);
      PageArr[Idx].PageObj := Child;
    end
    else if ChildType = 'Pages' then
    begin
      // intermediate node: recurse into its Kids
      BuildPageIndexFromTree(Child.GetArray('/Kids'), PageArr);
    end;
  end;
end;

Após esta execução, PageArr[0] é a primeira página que um visualizador exibiria, independentemente de onde esse objeto esteja no fluxo de bytes. Os índices passados por invocadores que assumem a ordem de exibição passam a mapear corretamente, e os erros de intervalo deixam de ocorrer.

Soluções temporárias hardcoded agravam o problema

Em bases de código onde a causa raiz nunca foi identificada, é comum encontrar correções heurísticas: inverter a primeira e última página se a contagem total for igual a 3, rodar o índice para documentos de um gerador específico ou aplicar um desvio quando o primeiro número de objeto exceder um limiar. Cada uma dessas correções serve exatamente para o conjunto de ficheiros de teste que estavam disponíveis quando foi escrita. Adicione uma fonte de PDF diferente e uma das correções será aplicada no momento errado, produzindo um índice que agora está duplamente incorreto: incorreto porque foi calculado a partir de uma matriz fora de ordem e incorreto novamente porque foi aplicada uma lógica inaplicável por cima. O verificador de intervalos deteta o erro mais à frente e o rastreamento da pilha (stack trace) aponta para um local inútil.

O único caminho produtivo é remover todos os mapeamentos heurísticos e substituir a construção da matriz de páginas por uma travessia de árvore adequada. Uma vez que os índices estejam corretos por construção, não são necessárias correções artificiais e a verificação de intervalo passa a ser uma aliada e não um obstáculo.

Se está a manter uma biblioteca que apresenta este padrão, ative temporariamente a verificação de intervalo numa compilação Release e execute-a contra um conjunto diversificado de PDFs: documentos produzidos por Word, LaTeX, firmware de scanners ou utilitários de divisão de PDF. Os ficheiros que disparam exceções são aqueles cuja ordem de objetos de página diverge da ordem de travessia que o seu código assume. Cada um deles é um ponto de dados relevante, não um erro isolado.

Para novo código que chama uma biblioteca PDF em Delphi, o conselho prático é tratar a contagem de páginas da biblioteca como autoritária e nunca passar um índice derivado de cálculos em dados externos sem antes confirmar que ele se situa em 0..PageCount - 1. O componente HotPDF expõe a contagem de páginas resolvida através de THotPDF.PageCount após BeginDoc ou após carregar um documento; esse valor reflete sempre a travessia da árvore de páginas e é seguro para utilização como limite superior para qualquer cálculo de índice.