Los errores de comprobación de rango (range check errors) en las bibliotecas PDF de Delphi tienen la reputación de ser difíciles de identificar porque no siguen un patrón de entrada consistente. El mismo documento los produce en un equipo y no en otro; la misma ruta de código genera la excepción en un archivo de 3 páginas pero se ejecuta sin problemas en uno de 12 páginas. Esa inconsistencia casi siempre se remonta a una sola causa raíz: los objetos de página del PDF no se almacenan en el orden del archivo. Si la biblioteca construye su arreglo interno de páginas escaneando objetos de forma secuencial en lugar de recorrer el árbol de páginas (page tree) declarado por el catálogo, construye un índice cuyo rango válido no coincide con lo que esperan los invocadores, y la comprobación de rango detecta esa falta de coincidencia en el peor momento posible
Cómo funciona la comprobación de rango en Delphi
Con la directiva de compilador {$R+} activa (el valor predeterminado en la configuración Debug), la RTL de Delphi valida cada índice de arreglo, subíndice de cadena y asignación enumerada en tiempo de ejecución. Un acceso fuera de los límites genera un ERangeError en lugar de leer silenciosamente la memoria adyacente. Ese comportamiento es valioso: saca a la luz los errores latentes de manera temprana en lugar de permitir que corrompan una estructura de datos que solo falla cien líneas después. La parte frustrante es que la excepción se dispara en el sitio de acceso, no en el punto donde el índice se calculó incorrectamente. Cuando la pila de llamadas (call stack) muestra un método profundamente anidado en una unidad PDF, el error real suele estar varios marcos atrás
Las condiciones booleanas compuestas empeoran esto. Delphi evalúa las expresiones and de izquierda a derecha con semántica de cortocircuito (short-circuit), pero el cortocircuito solo omite la evaluación cuando el lado izquierdo es False. Una expresión como:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
parece segura, pero solo protege contra un índice fuera de rango si FDocStarted es True y DestIndex no es negativo. La comprobación DestIndex < Length(PageArr) no hace nada cuando DestIndex es negativo, porque comparar un entero negativo con una longitud no negativa devuelve True en aritmética con signo y el acceso posterior al arreglo aún dispara el error de rango. Mover la comprobación de límites a la posición más externa es la solución correcta:
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 es la solución mecánica. Detiene el fallo. No explica por qué DestIndex recibió un valor fuera del rango válido en primer lugar
La verdadera causa: orden de los objetos versus orden de las páginas
ISO 32000-1 §7.7.3 define el árbol de páginas como un árbol de nodos Pages cuyos arreglos Kids enumeran los objetos de página en orden de visualización. El archivo almacena esos objetos en los desplazamientos (offsets) que el creador haya elegido; el objeto número 20 puede preceder físicamente al objeto número 3 en el flujo de bytes. Una biblioteca que construye su lista de páginas iterando la tabla de referencias cruzadas (cross-reference table) en el orden de los números de objeto en lugar de seguir la cadena Kids producirá una secuencia que difiere de lo que el usuario espera. En los documentos donde el generador escribió las páginas en orden, todo funciona. En los documentos donde no lo hizo, la discrepancia entre la numeración de páginas de la biblioteca y la numeración de páginas del invocador produce índices que caen fuera de PageArr
El enfoque correcto es comenzar desde el catálogo, resolver la referencia indirecta /Pages y recorrer el arreglo Kids de forma recursiva. Para un documento plano sin nodos Pages intermedios, el recorrido es directo:
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;
Después de que esto se ejecuta, PageArr[0] es la primera página que mostraría un visor, independientemente de dónde se encuentre ese objeto en el flujo de bytes. Los índices pasados por los invocadores que asumen el orden de visualización ahora se mapean correctamente y los errores de rango se detienen
Las soluciones alternativas codificadas de forma rígida agravan el problema
En las bases de código donde la causa raíz nunca se identificó, es común encontrar parches heurísticos: intercambiar la primera y la última página si el conteo total es igual a 3, rotar el índice para los documentos de un generador específico, aplicar un desplazamiento cuando el número del primer objeto excede un umbral. Cada uno de esos parches se ajusta exactamente al conjunto de archivos de prueba que se tenían a mano cuando se escribió. Agregue una fuente PDF diferente y uno de los parches se activará en el momento equivocado, produciendo un índice que ahora está doblemente equivocado: equivocado porque se calculó a partir de un arreglo desordenado y equivocado de nuevo porque se aplicó un mapeo inaplicable encima. El comprobador de rango lo detecta en algún lugar más adelante y el seguimiento de la pila no apunta a ningún lugar útil
El único camino productivo es eliminar cada mapeo heurístico y reemplazar la construcción del arreglo de páginas con un recorrido de árbol adecuado. Una vez que los índices son correctos por construcción, no se necesitan parches y el comprobador de rango se convierte en un activo en lugar de un obstáculo
Si está manteniendo una biblioteca que exhibe este patrón, habilite temporalmente la comprobación de rango en una compilación Release y ejecútela contra un corpus diverso de archivos PDF: documentos producidos por Word, por LaTeX, por firmware de escáneres, por utilidades de división de PDF a PDF. Los archivos que desencadenan excepciones son aquellos cuyo orden de objetos de página difiere del orden de recorrido que asume su código. Cada uno de ellos es un punto de datos, no un error (bug) separado
Para código nuevo que llama a una biblioteca PDF de Delphi, el consejo práctico es tratar el conteo de páginas de la biblioteca como autoritativo y nunca pasar un índice derivado de la aritmética en datos externos sin confirmar primero que se encuentra dentro de 0..PageCount - 1. El componente HotPDF expone el conteo de páginas resuelto a través de THotPDF.PageCount después de BeginDoc o después de cargar un documento; ese valor siempre refleja el recorrido del árbol de páginas y es seguro de usar como límite superior para cualquier aritmética de índices