Los errores de comprobación de rango en las bibliotecas PDF de Delphi tienen fama de ser difíciles de localizar porque no siguen un patrón de entrada coherente. El mismo documento los produce en una máquina pero no en otra; la misma ruta de código lanza la excepción en un fichero de 3 páginas pero funciona sin problemas en uno de 12. Esa inconsistencia casi siempre se remonta a una única causa raíz: los objetos de página PDF no se almacenan en el orden del fichero. Si la biblioteca construye su array de páginas interno escaneando objetos de forma secuencial en lugar de recorrer el árbol de páginas declarado por el catálogo, construye un índice cuyo rango válido no coincide con lo que esperan los llamantes, y la comprobación de rango detecta esa discrepancia 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), el RTL de Delphi valida en tiempo de ejecución cada índice de array, subíndice de cadena y asignación enumerada. Un acceso fuera de límites lanza ERangeError en lugar de leer silenciosamente la memoria adyacente. Ese comportamiento es valioso: saca a la luz errores latentes con antelación 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 punto de acceso, no en el punto donde el índice se calculó incorrectamente. Cuando la pila de llamadas muestra un método profundamente anidado en una unidad PDF, el error real suele estar varios fotogramas atrás.
Las condiciones booleanas compuestas empeoran esto. Delphi evalúa las expresiones and de izquierda a derecha con semántica de cortocircuito, 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 al array posterior sigue disparando el error de rango. Mover la comprobación de límites a la posición más externa es la correcció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 corrección mecánica. Detiene el fallo. No explica por qué DestIndex recibió un valor fuera del rango válido en primer lugar.
La causa real: orden de objetos frente a orden de páginas
ISO 32000-1 §7.7.3 define el árbol de páginas como un árbol de nodos Pages cuyos arrays Kids enumeran los objetos de página en orden de visualización. El fichero almacena esos objetos en los desplazamientos que el escritor haya elegido; el objeto número 20 puede preceder físicamente al objeto número 3 en el flujo de bytes. Una biblioteca que construya su lista de páginas iterando la tabla de referencias cruzadas en orden de número de objeto, en lugar de seguir la cadena Kids, producirá una secuencia que diverge de lo que espera el usuario. En documentos donde el generador haya escrito las páginas en orden, todo funciona. En documentos donde no lo haya hecho, la discrepancia entre la numeración de páginas de la biblioteca y la del llamante produce índices que quedan fuera de PageArr.
El enfoque correcto es partir del catálogo, resolver la referencia indirecta /Pages y recorrer el array Kids de forma recursiva. Para un documento plano sin nodos Pages intermedios, el recorrido es sencillo:
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;
Tras ejecutar esto, 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 llamantes que asumen el orden de visualización ahora se asignan correctamente y los errores de rango desaparecen.
Los workarounds codificados de forma fija agravan el problema
En bases de código donde la causa raíz nunca se identificó, es habitual encontrar parches heurísticos: intercambiar la primera y la última página si el total es 3, rotar el índice para documentos de un generador específico, aplicar un desplazamiento cuando el número del primer objeto supera un umbral. Cada uno de esos parches encaja exactamente con el conjunto de ficheros de prueba que estaban disponibles cuando se escribió. Añadid una fuente PDF diferente y uno de los parches se disparará en el momento equivocado, produciendo un índice que ahora es doblemente incorrecto: incorrecto porque se calculó a partir de un array desordenado, e incorrecto de nuevo porque se aplicó encima una asignación inaplicable. El comprobador de rango lo detecta en algún punto posterior y el seguimiento de pila no apunta a ningún lugar útil.
El único camino productivo es eliminar todos los mapeos heurísticos y reemplazar la construcción del array de páginas con un recorrido de árbol correcto. 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 mantenéis una biblioteca que presenta este patrón, activad la comprobación de rango en una compilación Release temporalmente y ejecutadla contra un corpus variado de PDFs: documentos producidos por Word, por LaTeX, por el firmware de un escáner, por utilidades de división PDF-a-PDF. Los ficheros que desencadenan excepciones son aquellos cuyo orden de objetos de página diverge del orden de recorrido que asume vuestro código. Cada uno es un punto de datos, no un error separado.
Para el nuevo código que llama a una biblioteca PDF de Delphi, el consejo práctico es tratar el recuento de páginas de la biblioteca como autoritativo y nunca pasar un índice derivado de aritmética sobre datos externos sin confirmar primero que está dentro de 0..PageCount - 1. El componente HotPDF expone el recuento de páginas resuelto mediante THotPDF.PageCount después de BeginDoc o tras cargar un documento; ese valor siempre refleja el recorrido del árbol de páginas y es seguro usarlo como límite superior para cualquier aritmética de índices.