PDFlibPas, la biblioteca de componente PDF VCL nativa para Delphi y C++Builder, reproduce el flujo de contenido de una página a través de su clase TPDFContentStateTracker sin tocar en absoluto un lienzo de renderizado. Alimentar al rastreador un operador analizado a la vez mantiene un registro de estado gráfico en curso —matriz de transformación actual, matriz de texto, límites de recorte, y la pila de guardado q/Q— disponible para captura instantánea antes o después de que se ejecute cada operador
Pregunte dónde aterriza realmente en la página impresa una ejecución de texto, y los números crudos del flujo de contenido por sí solos lo engañarán cada vez. TPDFContentProgram.GetTextRuns ya reporta el punto de anclaje de cada instrucción de mostrar texto mediante los campos OriginX y OriginY en TPDFTextRun, y los comentarios de campo son explícitos en que ese punto está en el espacio de texto, ya plegado a través de Tm, Td, TD, y T*. Lo que todavía falta, y lo que esos comentarios dicen que quien llama debe suministrar, es la CTM activa en esa instrucción exacta —el producto de cada cm concatenado hasta ese momento, anidado dentro de cualquiera que sea la cantidad de pares q/Q que resulten estar abiertos en ese punto del flujo
¿Por qué reproducir un flujo de contenido en lugar de renderizarlo?
PDFlibPas mantiene dos nociones separadas de estado gráfico para dos trabajos separados, y la división es deliberada. El registro de estado interno del renderizador lleva un handle de lienzo de dispositivo vivo, un handle de región de recorte, y cachés de rasterización de fuente —recursos reales atados a cualquiera que sea la superficie que se esté pintando actualmente, y sin significado en cuanto esa superficie desaparece. TPDFContentGraphicsState no lleva nada de eso: es un simple registro limitado a los valores que ISO 32000-1 §8.4 define como alcanzables solo desde operadores de flujo de contenido —la CTM, el estilo de línea, el color, el estado de texto, y los límites derivados de recorte y trazado. Como el registro no contiene ninguna referencia de lienzo ni ningún handle de archivo abierto, quien llama puede analizar un flujo de contenido, recorrerlo con TPDFContentStateTracker, y seguir usando las capturas resultantes mucho después de que lo que fuera que produjo los bytes haya desaparecido
Cómo construye la CTM TPDFContentStateTracker
TPDFContentStateTracker.Apply concatena los seis operandos de un operador cm en la CTM del rastreador usando la misma premultiplicación que especifica el propio PDF: la nueva matriz M2 se combina con la CTM actual como M2 × CTM, en la convención de vector fila donde un punto se transforma como P′ = P × M (ISO 32000-1 §8.4). La parte que es fácil de equivocar está en el término de traslación, no en la parte lineal: la propia traslación de M2 tiene que pasar por el componente de rotación y escala de la CTM actual antes de que se sume la traslación de la CTM actual encima. Sáltese ese paso y codifique en cambio de forma fija una combinación ingenua componente por componente, y el primer cm aislado que pruebe se verá correcto mientras cada coordenada aguas abajo de un segundo o tercer cm anidado se desvía silenciosamente, que es exactamente el tipo de bug que sobrevive la revisión de código porque la prueba unitaria que lo atraparía necesita al menos dos transformaciones encadenadas para fallar
var
Prog: TPDFContentProgram;
Runs: TPDFTextRunArray;
States: TPDFContentGraphicsStateArray;
DeviceX, DeviceY: Double;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
try
Prog.Parse(ContentBytes);
Runs := Prog.GetTextRuns;
// One before-instruction snapshot per operator, computed in a single pass
States := Prog.TraceGraphicsStates(nil, False);
for I := 0 to High(Runs) do
begin
// OriginX/OriginY already fold in Tm/Td/TD/T*; only the CTM active
// at this instruction is still missing (ISO 32000-1 8.4)
with States[Runs[I].InstructionIndex].CTM do
begin
DeviceX := Runs[I].OriginX * M11 + Runs[I].OriginY * M21 + DX;
DeviceY := Runs[I].OriginX * M12 + Runs[I].OriginY * M22 + DY;
end;
LogTextOrigin(Runs[I].Text, DeviceX, DeviceY); // caller-supplied handler
end;
finally
Prog.Free;
end;
end;
El bucle de arriba responde al punto de dolor de la apertura: TPDFContentProgram.GetTextRuns devuelve OriginX y OriginY ya plegados a través de Tm, Td, TD, y T*, y TraceGraphicsStates(nil, False) suministra la única pieza restante, la CTM previa a la instrucción en el índice exacto en el que se capturó cada ejecución, en una única pasada lineal sobre todo el programa. Pasar nil deja que el método posea un rastreador privado para la llamada y lo libere internamente, que es la elección correcta para un escaneo de una sola vez; pasar en cambio una instancia existente de TPDFContentStateTracker es lo que mantiene el estado continuo a través de una página ensamblada a partir de más de un flujo de contenido, ya que ISO 32000-1 trata el arreglo /Contents de una página como un único flujo lógico y la pila q/Q tiene que coincidir
La matriz de texto sobrevive a Q; el estado gráfico no
ISO 32000-1 §9.4.2 define Td, TD, Tm, y T* como los operadores que construyen la matriz de texto y la matriz de línea de texto dentro de un bloque BT/ET, y PDFlibPas mantiene esa distinción marcada: Td y TD concatenan una traslación pura sobre la matriz de línea de texto, T* hace lo mismo usando el negativo del interlineado actual, y solo Tm reemplaza ambas matrices por completo con los seis números que se le dan. BT reinicia ambas matrices a la identidad, exactamente una vez, al inicio del objeto de texto —pero q y Q no las tocan en absoluto. TPDFContentStateTracker.Apply trata coRestoreState como un caso especial precisamente por esta razón: antes de sacar de la pila el estado guardado, captura la matriz de texto actual, la matriz de línea de texto, y la bandera BT/ET, y las vuelve a aplicar sobre cualquiera que sea el estado que contenía lo sacado, porque un par q/Q envuelto alrededor de una ejecución de texto no se supone que mueva la posición del texto hacia atrás
var
Tracker: TPDFContentStateTracker;
Prog: TPDFContentProgram;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
Tracker := TPDFContentStateTracker.Create;
try
Prog.Parse('BT 100 700 Td q 2 0 0 2 0 0 cm (A) Tj Q (B) Tj ET');
for I := 0 to Prog.Count - 1 do
begin
Tracker.Apply(Prog[I]);
if Prog[I].Op in [coShowText, coRestoreState] then
LogState(Prog[I].OpName, Tracker.Snapshot); // caller-supplied handler
end;
finally
Tracker.Free;
Prog.Free;
end;
end;
Ejecute esa secuencia y la CTM reportada en el segundo Tj vuelve a la escala de identidad que tenía antes de q —el 2 0 0 2 0 0 cm dentro del par guardar/restaurar desapareció, como exige q/Q. TextMatrix.DX en esa misma instrucción, sin embargo, todavía es 100: el Td que lo estableció se ejecutó antes de la q, así que no es estado gráfico que la Q alguna vez tuviera derecho a tocar, y una herramienta que asumiera lo contrario reportaría la segunda ejecución de glifos empezando desde la posición horizontal equivocada en la página
¿Qué pasa cuando se ejecuta un operador de trazado de recorte?
Un operador W o W* no reduce el recorte de inmediato; solo registra qué regla de relleno usar, y la intersección real espera a cualquiera que sea el operador de pintado de trazado que le siga, incluido el pintador sin efecto n que los autores de PDF usan rutinariamente exactamente para recortar sin dibujar nada. TPDFContentStateTracker refleja precisamente esa temporización en dos pasos: coClip y coClipEvenOdd solo establecen una bandera de regla de recorte pendiente, y EndCurrentPath —invocado por cada operador de pintado de trazado— es lo que realmente intersecta los límites del trazado pendiente en ClipMinX, ClipMinY, ClipMaxX, y ClipMaxY. Acertar en esta puesta en escena importa para el propio contrato de captura antes/después: una captura previa tomada exactamente en la instrucción W todavía tiene que mostrar el recorte antiguo, más ancho, porque el recorte todavía no ha surtido efecto en ese punto del flujo, y colapsar los dos pasos en uno rompería silenciosamente a cada llamador que confía en que el estado previo signifique lo que dice
ClipBoundsExact le dice a quien llama cuál de dos situaciones está mirando, y solo es Verdadero para un único rectángulo alineado con los ejes construido por re sobre un trazado por lo demás vacío —la única forma que PDFlibPas puede representar exactamente como cuatro números. Todo lo demás —un rectángulo rotado, un contorno curvo, un trazado compuesto con varios subtrazados, o un recorte construido a partir de un modo de renderizado de texto— todavía produce ClipMinX a ClipMaxY, pero con ClipBoundsExact en Falso, una señal honesta de que los cuatro números son un límite exterior seguro y no la verdadera forma de recorte; los llamadores que solo necesitan ese límite, como aislar una subregión rectangular antes de la conversión descendente a semitono de GDI descrita en renderizar páginas de PDF a monocromo de 1 bit, pueden leerlo directamente en lugar de volver a derivarlo de la geometría de la página
Curvas Bézier: un límite exacto o uno seguro
La forma más barata de acotar un segmento de Bézier cúbico es tomar la envoltura convexa de sus cuatro puntos de control, y eso siempre es seguro porque la curva nunca sale de ella —pero una curva poco profunda y ancha puede reportar una caja delimitadora mucho más grande que lo que realmente ocupa la curva, lo que debilita el filtrado basado en recorte exactamente cuando más importa, en trazados decorativos grandes. PDFlibPas resuelve el problema más estricto en su lugar: para cada eje, resuelve la derivada de la curva cúbica en busca de raíces dentro del intervalo abierto (0, 1) y evalúa la curva en cualquier raíz que encuentre, junto con ambos extremos, que es la forma estándar de forma cerrada de obtener la verdadera extensión alineada con los ejes de una curva en lugar de una sobreestimación. La precisión por curva, sin embargo, no se traslada al recorte mismo: en cuanto un contorno curvo se convierte en un trazado de recorte, ClipBoundsExact igual cae a Falso para él, porque una caja delimitadora, por ajustada que sea, sigue sin ser la misma forma que la curva que delimita, y el rastreador de estado prefiere decirlo así antes que dejar que quien llama asuma un rectángulo donde en realidad hay una curva
Leer el estado antes y después de cada operador
Si quien llama quiere el estado previo o el posterior depende enteramente de qué hace el operador: una pregunta de dibujo o de detección de impacto sobre un trazado o ejecución de texto quiere el estado tal como estaba el instante antes de que se ejecutara ese operador, ya que eso es lo que realmente determinó cómo pintó el operador, mientras que una pregunta de diagnóstico sobre un operador que establece estado como gs normalmente quiere ver qué acaba de cambiar. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) expone exactamente esa elección como un único booleano, calculando un TPDFContentGraphicsState por instrucción en una pasada lineal sobre todo el programa sin importar qué instante se solicite. GetGraphicsState(InstructionIndex, AfterInstruction, State) ofrece la misma elección antes/después para una sola instrucción en lugar de todo el programa, pero reproduce desde la instrucción cero en cada llamada para llegar ahí, así que escanear muchos índices llamándolo en un bucle cuesta O(n²) frente a una única llamada O(n) a TraceGraphicsStates sobre el mismo programa
var
Before, After: TPDFContentGraphicsState;
begin
// Same instruction index, two different instants: before vs. after it runs
Prog.GetGraphicsState(CmIndex, False, Before);
Prog.GetGraphicsState(CmIndex, True, After);
// Before.CTM reflects every earlier cm; After.CTM already folds in
// this instruction's own concatenation as well
end;
Convivir con flujos de contenido malformados
Dos tipos de entrada malformada son lo bastante comunes en productores de PDF reales como para que TPDFContentStateTracker tenga que tolerarlos en lugar de fallar sobre ellos. El primero es un trazado que abarca un límite q/Q: el trazado actual, el punto actual, y el conteo de subtrazado no son parámetros de estado gráfico —ISO 32000-1 §8.4 cubre qué guardan y restauran q y Q, y el trazado actual en construcción no está entre ellos— así que TPDFContentStateTracker rastrea esos datos completamente fuera del estado guardado, y un subtrazado iniciado antes de una q sigue ahí, sin pintar, inmediatamente después de la Q correspondiente. El segundo es una Q suelta sin ninguna q correspondiente en ningún lugar anterior del flujo, no raro en la salida de generadores que ensamblan fragmentos de flujo de contenido por concatenación y se equivocan en la contabilidad. TPDFContentStateTracker.RestoreUnderflowCount cuenta cada uno de esos eventos en lugar de lanzar una excepción o corromper el estado: una Q sin par simplemente deja el estado gráfico actual exactamente como estaba, como si esa instrucción hubiera sido un no-op, así que el resto del flujo sigue reproduciéndose sobre un estado sensato y quien llama todavía puede decidir después, a partir del conteo, si vale la pena señalar la entrada de vuelta a quien la produjo
La composición de CTM, la independencia de la matriz de texto respecto a q/Q, y la realización escalonada de un trazado de recorte no dependen de cómo o si el flujo de contenido alguna vez se pinta, que es exactamente el punto: la misma captura de TPDFContentStateTracker es correcta ya sea que la página nunca se renderice en absoluto o esté a punto de entregarse a cualquiera que sea el backend que PDFlibPas seleccione para ese archivo, incluido el cambio de motor en tiempo de ejecución cubierto en la guía sobre renderizado de PDF multi-motor en PDFlibPas. El análisis de contenido, el mapeo de coordenadas, y las herramientas de redacción pueden todos ejecutarse enteramente sobre la salida del rastreador, mucho antes o completamente sin nunca pedirle a un renderizador que se involucre
La reproducción de flujo de contenido mediante TPDFContentStateTracker es parte del marco de edición de contenido estructurado integrado en PDFlibPas, la biblioteca de componente PDF VCL nativa para Delphi y C++Builder