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 una 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 misma página, y solo una cadena de llamadas que vuelve 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 ni con qué profundidad pueden invocarse Forms entre sí, así que un analizador conforme tiene que aceptar la reutilización legítima y el anidamiento legítimo sin dejar de defenderse contra la única disposición 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 a través de los valores TPDFlibContentFormTraversalStatus adjuntos a cada instantánea de Do, sobre todo 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 ciclo falso?
Una referencia repetida a un Form XObject no es, por sí sola, evidencia de nada equivocado. ISO 32000-1 permite invocar el mismo objeto Form 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 con número de página a lo largo de una página sin duplicar su flujo de contenido varias veces. La protección ingenua contra la recursión descontrolada es un único conjunto de visitados indexado por número de objeto: la primera vez que un recorredor ve el objeto Form 12, marca 12 como visto y se niega a entrar en él de nuevo en cualquier otro punto del árbol. Ese enfoque se rompe en cuanto el mismo sello aparece en dos esquinas sin relación de una misma 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 de Form resuelto a la cadena de llamadas activa inmediatamente antes de descender en él, y luego vuelve a sacar esa misma entrada en cuanto el descenso regresa, con éxito o sin él. Una invocación hermana del mismo flujo idéntico solo empieza después de que la primera ya se haya sacado, así que la cadena de llamadas está limpia de ese flujo para cuando la llamada hermana lo comprueba, y el recorredor la enumera exactamente igual que 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 sigue abierto en la cadena cuando su propio contenido vuelve a llamar a A, y A todavía sigue asentado en la cadena desde la llamada externa que todavía no ha regresado, esa es la única forma que reporta ftsCycle, un flujo de Form todavía abierto en algún punto anterior de la cadena de llamadas actual, no simplemente presente en algún otro sitio de la página
¿Hasta qué profundidad 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 TPDFlibContentFormTraversalStatus distintos precisamente por esa razón. Una cadena de veinte Forms 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 mal formado o adversario podría empujar arbitrariamente más alto si nada más lo detuviera. EnumPageContentStatesEx recibe un parámetro MaxFormDepth precisamente por esta razón y acota cualquiera que sea el valor que se le pase a un máximo de 64, sin importar lo que pida quien llama. Una profundidad de cero es un caso especial que merece la pena conocer por sí solo: deshabilita por completo la recursión de Form y reproduce el comportamiento plano, solo de página, del método más antiguo EnumPageContentStates, razón por la cual cada instantánea 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 subrastreador por invocación: aislar el estado gráfico
Cada descenso en un Form XObject obtiene su propio rastreador de estado gráfico en lugar de compartir el que ya está recorriendo la página, porque el flujo de contenido de un Form está obligado a dejar el estado gráfico exactamente como lo encontró, y PDFlibPas no puede asumir que cada PDF que abre respeta realmente ese requisito. El rastreador hijo parte de una instantánea de cualquiera que sea la CTM, el estado de color y los parámetros de texto activos en la instrucción Do que llamó, y luego reinicia su propia pila de guardar-y-restaurar y el seguimiento del trazado actual a vacío antes de ejecutar una sola instrucción del Form. Una q desequilibrada sin ninguna Q correspondiente dentro de un Form descuidado o dañado, algo no poco frecuente de encontrar en PDF producidos por herramientas más antiguas, queda 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 situada una línea más adelante en el flujo de contenido
El /Matrix del Form se compone con la CTM vigente en el Do del mismo modo que lo hace un operador cm, multiplicado por la izquierda contra la transformación actual en lugar de sustituirla, y PDFlibPas reutiliza deliberadamente esa misma vía de código en lugar de mantener una segunda fórmula, ya que dos implementaciones independientes de la misma álgebra de matrices son precisamente el tipo de duplicación que se desvía silenciosamente tras unas cuantas rondas de composición de escala, rotación y cizalladura. /BBox entonces recorta en el espacio de coordenadas propio del Form después de que ya se haya aplicado la matriz, y las cuatro esquinas de esa caja se transforman individualmente en lugar de solo las esquinas opuestas, ya que un Form rotado o cizallado puede de otro modo reportar un cuadro delimitador que se pierda contenido real situado en lo que antes era una esquina extrema antes de que la transformación lo moviera a otro sitio. Ampliar el bucle del ejemplo anterior sobre el mismo array 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 Forms 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 ámbito de recurso junto a cada nombre de recurso: cuando un Form lleva su propio diccionario /Resources, ese diccionario se convierte en el ámbito de recurso completo para todo lo que hay dentro, sin ningún respaldo clave a clave hacia el diccionario de la página o de quien llama para cualquier cosa que el propio diccionario del Form resulte omitir. Solo un Form sin ninguna clave /Resources en absoluto, un patrón que todavía producen algunos generadores de PDF más antiguos, hereda el diccionario que llama en bloque, y eso es una excepción de compatibilidad deliberada y no una regla general en la que merezca la pena apoyarse en salida nueva. La identidad de fuente en una instantánea TPDFlibContentGraphicsState es por tanto el par de FontResource y FontResourceScope, no solo el nombre, con FontObjectNumber disponible para confirmar exactamente a qué objeto indirecto se resolvió un /F1 dado en ese ámbito concreto
El mismo ámbito 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 las fuentes como caso especial, el caso de las fuentes simplemente resulta importar más, porque una identidad de fuente desajustada produce silenciosamente los glifos equivocados en lugar de un fallo evidente. El código de extracción que agrupa las tiradas de texto solo por nombre de fuente, sin agrupar también por ámbito de recurso, fusionará dos fuentes visualmente distintas que resulten compartir un nombre, y el error no saldrá a la luz hasta que alguien note que hay numerales de la tipografía equivocada dentro de lo que se suponía que se leería como una única fuente coherente
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 vuestro propio pipeline
FormTraversalStatus convierte por sí sola cada instantánea de Do en un pequeño informe de diagnóstico, y un pipeline que lo ignora está desperdiciando precisamente 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 antes de tiempo; y ftsMalformed cubre todo lo demás que detuvo el recorrido, una referencia de flujo irresoluble, un /Matrix o /BBox que no logró analizarse, o una excepción lanzada al ejecutar el propio contenido del Form. Ese último caso importa operativamente, porque un recorrido anidado fallido deshace 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 reventó 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, costes y dónde encaja esto
El flujo de contenido de un Form se decodifica y se analiza exactamente una vez por llamada de enumeración sin importar cuántas veces se invoque el Form, porque PDFlibPas cachea 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, no se decodifica tres veces. Lo que sí se reconstruye en cada invocación individual es todo lo que legítimamente difiere de un punto de llamada al siguiente: el rastreador hijo, la CTM concatenada, el recorte intersecado y el ámbito de recurso. Esa contabilidad de CTM y recorte por invocación es la misma maquinaria que hay detrás de el rastreador de estado de CTM y recorte de flujo de contenido de PDFlibPas, que merece la pena leer junto a este para cualquier recorrido de flujo de contenido que vaya más allá de la propia recursión de Form
Merece la pena fijar expectativas en torno a dos límites antes de que esta API entre en un pipeline mayor. El techo de profundidad de 64 niveles no es un mando de ajuste para documentos legítimamente profundos, ya que las facturas, extractos y plantillas de informe reales esencialmente nunca anidan Forms más de tres o cuatro niveles de profundidad, un documento que realmente alcanza ftsDepthLimit tiene muchas más probabilidades de estar mal formado o ser adversario que de ser inusualmente elaborado, y merece la pena registrarlo como señal de calidad de datos en lugar de reintentarlo silenciosamente con un número mayor. 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 aparte respondida por el estado de visibilidad de los grupos de contenido opcional cuando un Form de sello o marca de agua se sitúa 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 ámbito de recursos componen juntos un rincón de la superficie de inspección de flujo de contenido del componente PDFlibPas para Delphi y C++Builder