Os erros de verificação de limites (range check errors) nas bibliotecas PDF em Delphi ganharam a fama de serem de difícil diagnóstico por não seguirem um padrão de entrada consistente. O mesmo documento exibe-os numa máquina enquanto não acusa nada noutra; a mesma trajetória de código lança uma exceção num ficheiro de 3 páginas e corre na perfeição noutro com 12 páginas. A dita inconsistência reporta na sua esmagadora maioria a uma e única causa na sua base de raiz: os objetos de página PDF não se encontram armazenados na mesma ordem constante no ficheiro. Se a biblioteca edificar a sua matriz interna de páginas ao varrer sequencialmente os objetos, e não caminhando através da árvore das páginas declarada nos catálogos, irá assim conceber a construção num índice a cuja margem de validação deixará de corresponder com o preceito pretendido pelas chamadas em invocação, recaindo então a verificação de limites a apanhar a incongruência no pior instante possível
Como atua a verificação de limites (range checking) no Delphi
Com a diretiva ao compilador {$R+} ativa (a predefinição na configuração Debug), o Delphi RTL procede a validações, ao longo da execução da rotina, em todo e qualquer índice nos conjuntos array, bem como nas subscrições numéricas numa string de carateres e nas atribuições enumeradas. Ao detetar acessos desenquadrados além dos limites faz levantar um ERangeError ao invés de prosseguir e efetuar caladas leituras à memória envolvente. Essa atitude acarreta o seu valor: acaba a desvendar disfunções (bugs) latentes antes que possibilite o cenário à disrupção sobre as infraestruturas de matriz cujos dados apenas viriam a claudicar mais adiante ao atingir da próxima centena em linhas de extensão no documento. O aspeto frustrante passa pelo deflagrar sobre a ocorrência nas vias em acesso e instantes da transposição de referida exceção e não na zona em que o cálculo sobre o índice pautou ao impreciso pela via do insucesso. Ao deparar-se que a estrutura relativa à disposição nas chamadas aponta rumo ao mergulho aninhado entre linhas na unidade PDF, por força da regra tal constatação reflete-se muitas das ocasiões num erro assente no retrocesso numa dezena a várias passagens das esferas estruturais de acesso e tramas operacionais à retaguarda
Condições com expressões booleanas compostas não ajudam e tendem a exacerbar os males. O Delphi interpreta o percurso das abordagens via formulações com a junção and caminhando num fluxo e leitura a da esquerda em conformidade aos limites para com a direita recorrendo às referências pela métrica (short-circuit) de curto-circuito semântico. A premissa referida contorna porém em saltos as devidas interpretações num único cenário: ao constatar sobre as esquerdas uma valia assente pelo False. Tome por consideração este caso prefigurado à imagem do exposto numa expressão com esta via:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
Isto parece seguro, mas só protege contra índices fora do limite se FDocStarted for True e DestIndex não for negativo. A verificação DestIndex < Length(PageArr) não faz nada quando DestIndex é negativo. Comparar um número negativo com um comprimento não negativo devolve True em aritmética com sinal. Logo, o acesso à matriz (array) dispara o erro de limite. Mover a verificação de limites para a posição mais externa é a soluçã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 solução mecânica. Impede a falha e bloqueio. Porém, não explica o motivo de DestIndex ter recebido um valor fora do limite válido
A verdadeira causa: ordem dos objetos contra a 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. As matrizes Kids listam os objetos das páginas na sua ordem de apresentação. O ficheiro armazena estes objetos em qualquer posição (offset) que o criador tenha escolhido; o objeto 20 pode anteceder o objeto 3 no fluxo de bytes. Uma biblioteca que construa a sua lista iterando a tabela de referência cruzada na ordem numérica dos objetos, em vez de seguir a cadeia de Kids, produzirá uma sequência que diverge do esperado. Nos documentos onde os objetos foram escritos de forma sequencial, tudo funciona. Quando tal não acontece, a discrepância entre a numeração gerada pela biblioteca e a referenciada na chamada causa índices errados em PageArr
A abordagem correta passa por iniciar a leitura do catálogo, resolver a referência indireta /Pages e seguir a matriz Kids de forma recursiva. Para um documento simples sem nós intermediários de Pages, a passagem direta é simples:
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;
Terminada a execução da rotina, PageArr[0] representa a primeira página que o visor exibe, independentemente do local que ocupe no fluxo de bytes. Os índices indicados nos chamamentos ficam assim assentes num mapeamento correto, resolvendo-se as incorreções da verificação de limites (range errors)
Soluções forçadas de contorno agravam o problema
Em bases de código em que a causa primária nunca foi apurada, é frequente encontrarmos arranjos (patches) baseados em heurística: trocar a primeira e a última página num ficheiro com 3 páginas, rodar o índice para documentos criados por um motor de geração específico, ou aplicar desvios quando o primeiro número de objeto ultrapassa um limiar. Estes arranjos encaixam e resultam apenas na amostra de ficheiros utilizada para os testes no momento. Ao adicionar uma fonte de PDF distinta, um dos arranjos falha e ativa-se na altura errada, elaborando um índice duplamente desadequado: está errado por via da matriz desordenada e novamente danificado pela aplicação incorreta e imprópria sobre o mapeamento associado. A verificação falha nalguma posição a jusante e não faculta informações úteis face à falha e à base da origem
A única trajetória produtiva consiste na eliminação dos mapeamentos de base e cunho na natureza da heurística, com a correspondente e inerente adoção pela substituição na estrutura matricial para as matrizes pelas chamadas corretas à travessia da árvore de disposição em cada nó (tree walk). Assim que os índices adquirirem a ordem natural por intermédio e vias da correta formulação e estrutura edificada no vetor na raiz em base pela via do código, os arranjos de correção em (patches) ficam perfeitamente isentos de justificação e o recurso a verificadores (range checker) converte-se num ativo e deixa de constituir um obstáculo
Caso se encontre a efetuar e a assegurar serviços na manutenção de bibliotecas de software cujo desempenho apresente estes exatos sintomas e comportamentos, ative a verificação de limites (range checking) temporariamente numa compilação de modo Release. Submeta o seu código a diversos e múltiplos testes de processamento englobando diferentes PDF de fontes distintas: relatórios feitos pelo Word, pelo LaTeX, pelo software de um digitalizador e ficheiros gerados em utilitários de separação de PDF. Os ficheiros que gerem exceção correspondem àqueles cujas ordens nos objetos divergem das ordens de navegações que o código assume. Cada ocorrência num de constitui um novo ponto de referência e não um bug isolado
Para novos códigos em uso e que chamem bibliotecas Delphi em PDF, a atitude aconselhável é tratar a contagem de páginas da biblioteca como o valor oficial e absoluto. Nunca passe um índice originado a partir de cálculos perante dados externos sem comprovar em primeiro lugar se o mesmo recai no âmbito dos limites de 0..PageCount - 1. O Componente HotPDF exibe a contagem de páginas resolvida no interior de THotPDF.PageCount sempre no momento que se segue ao BeginDoc ou nos carregamentos do documento PDF. Este valor demonstra e reflete a passagem através da árvore das páginas e é sempre confiável para que possa assentar nesse os preceitos de limite máximo para eventuais cálculos de índices