PDFlibPas resuelve las llamadas recursivas a Form XObject en flujos de contenido PDF de Delphi rastreando la cadena de llamadas activa, no un conjunto global de visitados, así que TPDFlib.EnumPageContentStatesEx puede recorrer el mismo Form invocado varias veces en una página sin confundir la reutilización legítima con un ciclo. Un Form XObject de sello en una plantilla de factura es el caso típico: el mismo objeto se llama desde el encabezado, el pie de página, y una capa de marca de agua en una página, y solo una cadena de llamadas que se cierra sobre sí misma es un ciclo genuino
ISO 32000-1 §8.10 define un Form XObject como un flujo de contenido autocontenido que una página, u otro Form, invoca con el operador Do, completo con su propio sistema de coordenadas en /Matrix, un límite de recorte en ese sistema de coordenadas en /BBox, y opcionalmente su propio diccionario de recursos. Nada en la especificación limita cuántas veces se puede invocar un Form o con qué profundidad los Form pueden invocarse entre sí, así que un analizador conforme tiene que aceptar la reutilización legítima y el anidamiento legítimo mientras aún se defiende contra el único arreglo que la especificación sí prohíbe: un Form cuyo flujo de contenido, directa o transitivamente, se invoca a sí mismo. PDFlibPas reporta esa distinción mediante los valores TPDFlibContentFormTraversalStatus adjuntos a cada captura de Do, más notablemente ftsEnumerated para un descenso exitoso y ftsCycle para el único caso que realmente es un bucle
¿Por qué reutilizar el mismo Form XObject no dispara un falso ciclo?
Una referencia repetida a Form XObject no es, por sí sola, evidencia de que algo esté mal. ISO 32000-1 permite que el mismo objeto Form se invoque desde tantos lugares de un flujo de contenido como quiera el autor, que es exactamente cómo se reutiliza un sello de logotipo, una plantilla de membrete, o un pie de página de número de página a través de una página sin duplicar su flujo de contenido varias veces. La protección ingenua contra recursión desbocada es un único conjunto de visitados indexado por número de objeto: la primera vez que un recorredor ve el objeto Form 12, marca el 12 como visto y se niega a volver a entrar en él en cualquier otro lugar del árbol. Ese enfoque se rompe en cuanto el mismo sello aparece en dos esquinas no relacionadas de una página, porque la segunda llamada, enteramente legítima, llega después de que el número de objeto ya está marcado como visto y se rechaza como si fuera un bucle
PDFlibPas evita ese falso positivo acotando la detección de ciclos a la cadena de llamadas actual en lugar de a todo el documento. EnumPageContentStatesEx empuja el flujo Form resuelto a la cadena de llamadas activa inmediatamente antes de descender en él, y luego saca esa misma entrada de nuevo en el momento en que el descenso retorna, con éxito o no. Una invocación hermana del mismo flujo idéntico solo empieza después de que la primera ya se sacó, así que la cadena de llamadas está libre de ese flujo para cuando la llamada hermana lo comprueba, y el recorredor lo enumera exactamente como lo haría con cualquier otro Form. Un ciclo verdadero se ve distinto en esa misma cadena: el Form A llama al Form B, B todavía está abierto en la cadena cuando su propio contenido llama de vuelta a A, y A todavía está sentado en la cadena desde la llamada externa que todavía no ha retornado —esa es la única forma que reporta ftsCycle, un flujo Form todavía abierto en algún lugar anterior de la cadena de llamadas actual, no meramente presente en algún otro lugar de la página
¿Cuán profundo puede llegar la recursión de Form XObject antes de que PDFlibPas la detenga?
La detección de ciclos y la limitación de profundidad resuelven dos problemas distintos, y PDFlibPas los mantiene como dos resultados distintos de TPDFlibContentFormTraversalStatus exactamente por esa razón. Una cadena de veinte Form distintos, cada uno llamando al siguiente y ninguno repitiéndose, no es un ciclo bajo ninguna definición —la comprobación de cadena activa nunca encuentra un flujo repetido— pero veinte niveles honestos de anidamiento siguen siendo veinte niveles de análisis, concatenación de matriz, y resolución de recursos que un PDF malformado o adversarial podría empujar arbitrariamente más alto si nada más lo detuviera. EnumPageContentStatesEx toma un parámetro MaxFormDepth exactamente por esta razón y limita cualquiera que sea el valor pasado a un máximo de 64, sin importar qué pida quien llama. Una profundidad de cero es un caso especial que vale la pena conocer por sí solo: deshabilita la recursión de Form por completo y reproduce el comportamiento plano, de solo página, del método más antiguo EnumPageContentStates, que es por qué cada captura de Do en ese modo reporta ftsNotRequested en lugar de intentar nada
var
Lib: TPDFlib;
States: array of TPDFlibContentGraphicsState;
Count, I: Integer;
begin
Lib:= TPDFlib.Create;
try
if Lib.LoadFromFile('invoice-batch.pdf', '')<> 1 then
Exit;
Lib.SelectPage(1);
Count:= Lib.EnumPageContentStatesEx(True, 8, States); // count only
SetLength(States, Count);
Lib.EnumPageContentStatesEx(True, 8, States); // fill
for I:= 0 to Count- 1 do
if States[I].FormTraversalStatus= ftsCycle then
LogSuspectForm(States[I].XObjectResource, States[I].ContentDepth);
finally
Lib.Free;
end;
end;
Un sub-rastreador por invocación: aislar el estado gráfico
Cada descenso en un Form XObject recibe su propio rastreador de estado gráfico en lugar de compartir el que ya recorre la página, porque se exige que el flujo de contenido de un Form deje el estado gráfico exactamente como lo encontró, y PDFlibPas no puede asumir que cada PDF que abre realmente honra ese requisito. El rastreador hijo empieza a partir de una captura de cualquiera que sea la CTM, estado de color, y parámetros de texto activos en la instrucción Do que llamó, y luego reinicia su propia pila de guardar-y-restaurar y el rastreo de trazado actual a vacío antes de ejecutar una sola instrucción del Form. Una q sin balancear sin ninguna Q correspondiente dentro de un Form descuidado o dañado, no algo raro de encontrar en PDF producidos por herramientas más antiguas, se mantiene contenida dentro del rastreador de esa única invocación y nunca se filtra a la página del rastreador ni a una invocación hermana del mismo sello sentada una línea después en el flujo de contenido
/Matrix del Form se compone con la CTM en efecto en la Do de la misma manera que lo hace un operador cm, multiplicada por la izquierda contra la transformación actual en lugar de sustituirla, y PDFlibPas deliberadamente reutiliza esa única ruta de código en lugar de mantener una segunda fórmula, ya que dos implementaciones independientes de la misma álgebra de matrices son exactamente el tipo de duplicación que silenciosamente se desvía después de unas cuantas rondas de composición de escala, rotación, y sesgo. /BBox luego recorta en el propio espacio de coordenadas del Form después de que la matriz ya se ha aplicado, y las cuatro esquinas de esa caja se transforman individualmente en lugar de solo las esquinas opuestas, ya que un Form rotado o sesgado puede de otro modo reportar una caja delimitadora que se pierde contenido real sentado en lo que solía ser una esquina extrema antes de que la transformación lo moviera a otro lugar. Extender el bucle del ejemplo anterior sobre el mismo arreglo States lee esos campos directamente
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Do')and (States[I].XObjectKind= cxkForm)and
States[I].FormBBoxKnown then
Writeln('Form ', States[I].XObjectResource, ' matrix ',
States[I].FormMatrix.M11:0:3, ',', States[I].FormMatrix.M12:0:3,
' bbox ', States[I].FormBBoxLeft:0:1, '..', States[I].FormBBoxRight:0:1);
¿Comparten fuente dos Form con el mismo nombre de recurso?
No. Un nombre de recurso como /F1 solo significa algo relativo al diccionario de recursos activo en el punto donde se usa, y dos Form XObject distintos son libres de definir dos fuentes completamente distintas bajo ese nombre idéntico. PDFlibPas resuelve esto rastreando un alcance de recurso junto a cada nombre de recurso: cuando un Form lleva su propio diccionario /Resources, ese diccionario se convierte en el alcance de recurso completo para todo lo que hay dentro de él, sin ningún respaldo por clave a la página o diccionario que llama para cualquiera que sea la omisión del propio diccionario del Form. Solo un Form sin ninguna clave /Resources en absoluto, un patrón todavía producido por algunos generadores de PDF más antiguos, hereda el diccionario que llama en bloque, y eso es una excepción de compatibilidad deliberada en lugar de una regla general en la que valga la pena apoyarse en salida nueva. La identidad de fuente en una captura de TPDFlibContentGraphicsState es por lo tanto el par de FontResource y FontResourceScope, no el nombre solo, con FontObjectNumber disponible para confirmar exactamente a qué objeto indirecto se resolvió un /F1 dado en ese alcance particular
El mismo acotamiento se aplica a cualquier otro recurso con nombre que pueda llevar un Form, entradas ExtGState y entradas XObject anidadas incluidas, ya que el mecanismo de resolución subyacente no trata a las fuentes como un caso especial —el caso de fuente simplemente resulta importar más, porque una identidad de fuente desajustada produce silenciosamente los glifos equivocados en lugar de un fallo obvio. El código de extracción que agrupa ejecuciones de texto solo por nombre de fuente, sin agrupar también por alcance de recurso, fusionará dos fuentes visualmente distintas que resulten compartir un nombre, y el error no saldrá a la superficie hasta que alguien note números de una tipografía equivocada sentados dentro de lo que se suponía debía leerse como una fuente consistente
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Tj')and States[I].TextAdvanceResolved then
RecordGlyphRun(States[I].FontResource, States[I].FontResourceScope,
States[I].FontObjectNumber, States[I].ContentDepth);
Leer FormTraversalStatus en su propio pipeline
FormTraversalStatus convierte cada captura de Do en un pequeño informe de diagnóstico por sí solo, y un pipeline que lo ignora está desechando exactamente la información que explicaría una extracción incompleta. ftsNotApplicable significa que la instrucción nunca fue una invocación de Form resuelta en primer lugar; ftsNotRequested significa que la recursión estaba desactivada para esta llamada; ftsEnumerated significa que el Form se analizó y recorrió con éxito; ftsDepthLimit y ftsCycle marcan las dos formas en que un descenso se corta a propósito; y ftsMalformed cubre todo lo demás que detuvo el recorrido —una referencia de flujo irresoluble, un /Matrix o /BBox que falló al analizarse, o una excepción lanzada al ejecutar el propio contenido del Form. Ese último caso importa operacionalmente, porque un recorrido anidado fallido revierte cualquier salida parcial que ya hubiera producido para esa rama, así que quien llama nunca tiene que adivinar si un Form estaba genuinamente vacío o simplemente explotó dos instrucciones dentro de su flujo de contenido
var
Tally: array[TPDFlibContentFormTraversalStatus] of Integer;
Status: TPDFlibContentFormTraversalStatus;
begin
for Status:= Low(Tally) to High(Tally) do
Tally[Status]:= 0;
for I:= 0 to Count- 1 do
Inc(Tally[States[I].FormTraversalStatus]);
if (Tally[ftsCycle]> 0)or (Tally[ftsMalformed]> 0) then
FlagForManualReview(SourceFileName, Tally[ftsCycle], Tally[ftsMalformed]);
end;
Límites, costos, y dónde encaja esto
El flujo de contenido de un Form se decodifica y analiza exactamente una vez por llamada de enumeración sin importar cuántas veces se invoque el Form, porque PDFlibPas guarda en caché la lista de instrucciones analizada contra el objeto de flujo subyacente en lugar de volver a analizarla en cada llamada hermana —el sello de tres esquinas del ejemplo de apertura se decodifica una vez y se recorre tres veces, no se decodifica tres veces. Lo que sí se reconstruye en cada invocación individual es todo lo que legítimamente difiere de un sitio de llamada al siguiente: el rastreador hijo, la CTM concatenada, el recorte intersectado, y el alcance de recurso. Esa contabilidad de CTM y recorte por invocación es la misma maquinaria detrás de el rastreador de estado de CTM y recorte de flujo de contenido de PDFlibPas, que vale la pena leer junto con este para cualquier recorrido de flujo de contenido que vaya más allá de la propia recursión de Form
Dos límites vale la pena fijar expectativas alrededor de ellos antes de que esta API entre en un pipeline más grande. El techo de profundidad de 64 niveles no es una perilla de ajuste para documentos legítimamente profundos, ya que las facturas, estados de cuenta, y plantillas de informe reales esencialmente nunca anidan Form más de tres o cuatro niveles de profundidad —un documento que realmente alcanza ftsDepthLimit tiene muchas más probabilidades de estar malformado o ser adversarial que de ser inusualmente elaborado, y vale la pena registrarlo como una señal de calidad de datos en lugar de reintentarlo silenciosamente con un número más grande. EnumPageContentStatesEx también es una API de análisis del lado de lectura: reporta qué hace un flujo de contenido, no si un Form debería ser visible en absoluto, que es una pregunta separada que responde el estado de visibilidad de Grupo de Contenido Opcional cuando un Form de sello o marca de agua está detrás de una capa que un visor podría haber desactivado. La detección de ciclos por cadena de llamadas, el aislamiento por invocación, y el acotamiento de recursos juntos conforman un rincón de la superficie de inspección de flujo de contenido en el componente PDFlibPas para Delphi y C++Builder