PDFlibPas, la biblioteca de componente PDF VCL nativo 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 cada vez mantiene disponible un registro de estado gráfico en curso, matriz de transformación actual, matriz de texto, límites de recorte y pila de guardado q/Q, listo para tomar una instantánea antes o después de que se ejecute cada operador
Preguntad dónde aterriza realmente una tirada de texto en la página impresa, y los números en bruto del flujo de contenido por sí solos os van a engañar siempre. TPDFContentProgram.GetTextRuns ya informa del punto ancla 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 reside en 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 cuantos pares q/Q 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 en vivo, un handle de región de recorte y cachés de rasterización de fuente, recursos reales vinculados a cualquiera que sea la superficie que se esté pintando en ese momento, y sin sentido en cuanto esa superficie desaparece. TPDFContentGraphicsState no lleva nada de eso: es un registro plano limitado a los valores que ISO 32000-1 §8.4 define como alcanzables únicamente 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 instantáneas resultantes mucho después de que haya desaparecido cualquiera que sea lo que produjo los bytes
Cómo construye TPDFContentStateTracker la CTM
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 matriz nueva 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 fácil de hacer mal reside 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 encima la traslación de la CTM actual. Saltaos ese paso y codificad en su lugar de forma fija una combinación ingenua componente a componente, y el primer cm aislado que probéis parecerá correcto mientras cada coordenada aguas abajo de un segundo o tercer cm anidado se desvía silenciosamente, exactamente el tipo de fallo que sobrevive a la revisión de código porque la prueba unitaria que lo detectarí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 pieza restante, la CTM previa a la instrucción en el índice exacto en que se capturó cada tirada, en una única pasada lineal sobre todo el programa. Pasar nil deja que el método sea propietario de un rastreador privado para la llamada y lo libere internamente, la elección correcta para un escaneo puntual; pasar en su lugar una instancia TPDFContentStateTracker existente 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 array /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 nítida: 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 sustituye ambas matrices por completo por los seis números que recibe. BT reinicia ambas matrices a la identidad, exactamente una vez, al principio del objeto de texto, pero q y Q no las tocan en absoluto. TPDFContentStateTracker.Apply trata coRestoreState como caso especial precisamente por esta razón: antes de sacar el estado guardado de la pila, captura la matriz de texto actual, la matriz de línea de texto y el indicador BT/ET, y los vuelve a aplicar sobre lo que fuera que contuviera el estado sacado, porque un par q/Q que envuelva una tirada de texto no debería mover la posición de texto de vuelta 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;
Ejecutad 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 ha desaparecido, tal como exige q/Q. TextMatrix.DX en esa misma instrucción, sin embargo, sigue siendo 100: el Td que lo estableció se ejecutó antes del q, así que no es estado gráfico que la Q tuviera jamás derecho a tocar, y una herramienta que asumiera lo contrario reportaría la segunda tirada de glifos empezando en la posición horizontal equivocada de la página
¿Qué ocurre cuando se ejecuta un operador de trazado de recorte?
Un operador W o W* no encoge 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 lo siga, incluido el pintador sin efecto n que los autores de PDF usan rutinariamente precisamente para recortar sin dibujar nada. TPDFContentStateTracker refleja esa temporización de dos pasos con precisión: coClip y coClipEvenOdd solo establecen un indicador de regla de recorte pendiente, y EndCurrentPath, invocado por cada operador de pintado de trazado, es lo que realmente interseca los límites del trazado pendiente en ClipMinX, ClipMinY, ClipMaxX y ClipMaxY. Acertar con esta puesta en escena importa para el propio contrato de instantánea antes/después: una instantánea 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íe en que el estado previo signifique lo que dice
ClipBoundsExact le dice a quien llama cuál de dos situaciones está viendo, y solo es True 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, sigue produciendo ClipMinX hasta ClipMaxY, pero con ClipBoundsExact puesto a False, 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 halftone GDI descrita en el renderizado de páginas 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 Bézier cúbico es tomar la envolvente convexa de sus cuatro puntos de control, y eso siempre es seguro porque la curva nunca la abandona, pero una curva plana y ancha puede reportar un cuadro delimitador mucho mayor que lo que realmente ocupa la curva, lo que debilita el filtrado basado en recorte precisamente cuando más importa, en trazados decorativos grandes. PDFlibPas resuelve en cambio el problema más ajustado: 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 cualesquiera raíces que encuentre, junto con ambos extremos, que es la forma estándar de forma cerrada de obtener la extensión real alineada con los ejes de una curva en lugar de una sobreestimación. La precisión por curva, sin embargo, no se traslada al propio recorte: en cuanto un contorno curvo se convierte en un trazado de recorte, ClipBoundsExact sigue cayendo a False para él, porque un cuadro delimitador, por ajustado 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
Que quien llama quiera el estado previo o el posterior depende por completo de lo que haga el operador: una pregunta de dibujo o de prueba de impacto sobre un trazado o una tirada 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 única 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 única instrucción en lugar de para todo el programa, pero reproduce desde la instrucción cero en cada llamada para llegar hasta 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 mal formados
Dos tipos de entrada mal formada son lo bastante comunes en productores de PDF reales como para que TPDFContentStateTracker tenga que tolerarlos en lugar de fallar con ellos. El primero es un trazado que atraviesa un límite q/Q: el trazado actual, el punto actual y el recuento de subtrazados no son parámetros de estado gráfico, ISO 32000-1 §8.4 cubre lo que q y Q guardan y restauran, y el trazado que se está construyendo actualmente no está entre ellos, así que TPDFContentStateTracker rastrea esos datos completamente fuera del estado guardado, y un subtrazado empezado antes de un q sigue ahí, sin pintar, inmediatamente después de la Q correspondiente. El segundo es una Q suelta sin ningún q correspondiente antes de ella en ningún punto del flujo, no poco frecuente 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 pareja 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 recuento, si la entrada merece señalarse de vuelta a quien la produjo
La composición de la CTM, la independencia de la matriz de texto respecto a q/Q, y la realización por etapas de un trazado de recorte no dependen de cómo o si el flujo de contenido llega jamás a pintarse, que es precisamente la cuestión: la misma instantánea de TPDFContentStateTracker es correcta tanto si la página nunca se renderiza en absoluto como si 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 PDF multimotor en PDFlibPas. El análisis de contenido, el mapeo de coordenadas y las herramientas de redacción pueden ejecutarse todos enteramente sobre la salida del rastreador, mucho antes o completamente sin pedir jamás que intervenga un renderizador
La reproducción de flujo de contenido a través de TPDFContentStateTracker forma parte del marco de edición de contenido estructurado incorporado en PDFlibPas, la biblioteca de componente PDF VCL nativo para Delphi y C++Builder