Erros de range check (verificação de limites) em bibliotecas PDF Delphi têm a reputação de serem difíceis de identificar porque não seguem um padrão de entrada consistente. O mesmo documento os produz em uma máquina e não em outra; o mesmo caminho de código dispara a exceção em um arquivo de 3 páginas, mas é executado sem problemas em um de 12 páginas. Essa inconsistência quase sempre aponta para uma única causa raiz: os objetos de página do PDF não são armazenados na ordem do arquivo. Se a biblioteca construir seu array interno de páginas verificando os objetos sequencialmente, em vez de percorrer a árvore de páginas declarada pelo catálogo, ela construirá um índice cujo intervalo válido não corresponde ao que os chamadores esperam, e a verificação de limites detecta essa incompatibilidade no pior momento possível
Como funciona o range checking no Delphi
Com a diretiva de compilador {$R+} ativa (o padrão na configuração Debug), a RTL do Delphi valida cada índice de array, subscrito de string e atribuição enumerada em tempo de execução. Um acesso fora dos limites levanta um ERangeError em vez de ler silenciosamente a memória adjacente. Esse comportamento é valioso: ele traz à tona bugs latentes cedo, em vez de deixá-los corromper uma estrutura de dados que só falha 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 mostra um método profundamente aninhado em uma unidade PDF, o verdadeiro erro geralmente está alguns frames atrás
Condições booleanas compostas pioram isso. O Delphi avalia expressões and da esquerda para a direita com semântica de curto-circuito, mas o curto-circuito só pula 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 ela só protege contra um índice fora do intervalo se FDocStarted for True e DestIndex não for negativo. A verificação DestIndex < Length(PageArr) não faz nada quando DestIndex é negativo, porque comparar um inteiro negativo com um comprimento não-negativo retorna True na aritmética com sinal e o acesso subsequente ao array ainda dispara o erro de limite. Mover a verificação de limites para a posição mais externa é a correção certa:
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. Ela para o travamento. Ela não explica por que DestIndex recebeu um valor fora do intervalo válido em primeiro lugar
A verdadeira causa: ordem do objeto versus ordem da página
A ISO 32000-1 §7.7.3 define a árvore de páginas como uma árvore de nós Pages cujos arrays Kids listam os objetos de página na ordem de exibição. O arquivo armazena esses objetos nos offsets que o criador escolheu; o objeto número 20 pode preceder fisicamente o objeto número 3 no fluxo de bytes. Uma biblioteca que constrói sua lista de páginas iterando a tabela de referência cruzada na ordem do número do objeto, em vez de seguir a cadeia de Kids, produzirá uma sequência que diverge do que o usuário espera. Em documentos onde o gerador acidentalmente 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 numeração de páginas do chamador produz índices que ficam fora de PageArr
A abordagem correta é começar do catálogo, resolver a referência indireta de /Pages e percorrer o array Kids recursivamente. Para um documento plano sem nós intermediários de Pages, 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;
Depois que isso é executado, PageArr[0] é a primeira página que um visualizador exibiria, independentemente de onde esse objeto se encontre no fluxo de bytes. Os índices passados por chamadores que assumem a ordem de exibição agora são mapeados corretamente e os erros de limite param
Soluções alternativas hard-coded agravam o problema
Em bases de código onde a causa raiz nunca foi identificada, é comum encontrar patches heurísticos: trocar a primeira e a última página se a contagem total for igual a 3, rotacionar o índice para documentos de um gerador específico, aplicar um deslocamento quando o primeiro número de objeto excede um limite. Cada um desses patches se ajusta exatamente ao conjunto de arquivos de teste que estavam disponíveis quando ele foi escrito. Adicione uma fonte PDF diferente e um dos patches dispara na hora errada, produzindo um índice que agora está duplamente errado: errado porque foi calculado a partir de um array fora de ordem, e errado de novo porque um mapeamento inaplicável foi aplicado por cima. O verificador de limites o detecta em algum lugar a jusante e o rastreamento da pilha não aponta para nenhum lugar útil
O único caminho produtivo é remover todo mapeamento heurístico e substituir a construção do array de páginas por uma travessia de árvore adequada. Uma vez que os índices estejam corretos desde a construção, nenhum patch será necessário e o verificador de limites se torna um trunfo em vez de um obstáculo
Se você estiver mantendo uma biblioteca que exibe esse padrão, ative a verificação de limites em um build Release temporariamente e execute-o contra um corpus diverso de PDFs: documentos produzidos por Word, por LaTeX, por firmware de scanner, por utilitários de divisão PDF-para-PDF. Os arquivos que disparam exceções são aqueles cuja ordem de objetos de página diverge da ordem de travessia que seu código assume. Cada um deles é um ponto de dado, não um bug separado
Para um código novo que chama uma biblioteca PDF Delphi, o conselho prático é tratar a contagem de páginas da biblioteca como confiável e nunca passar um índice derivado de aritmética sobre dados externos sem primeiro confirmar se ele se enquadra em 0..PageCount - 1. O componente HotPDF expõe a contagem de páginas resolvida por meio de THotPDF.PageCount após o BeginDoc ou após o carregamento de um documento; esse valor sempre reflete a travessia da árvore de páginas e é seguro para ser usado como limite superior para qualquer aritmética de índice