Artículo técnico

Estado de flujo de contenido en PDF Library for Delphi: seguimiento de CTM y recorte

PDF Library for Delphi, 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

Diagrama de PDF Library for Delphi del pipeline de repetición del flujo de contenido: los operadores PDF analizados alimentan a TPDFContentStateTracker un operador a la vez, que mantiene la CTM, la matriz de texto, los límites de recorte y la pila q/Q como un record de instantánea
Cada operador analizado actualiza el registro de estado gráfico del rastreador, y una única pasada lineal produce una instantánea por instrucción. El registro sigue siendo utilizable sin que intervenga ningún lienzo de renderizado

¿Por qué reproducir un flujo de contenido en lugar de renderizarlo?

PDF Library for Delphi 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;
    // Un snapshot previo a la instrucción por cada operador, calculado en una sola pasada
    States := Prog.TraceGraphicsStates(nil, False);
    for I := 0 to High(Runs) do
    begin
      // OriginX/OriginY ya incorporan Tm/Td/TD/T*; solo falta el CTM activo
      // en esta instrucción (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 PDF Library for Delphi 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

Cronología de PDF Library for Delphi del flujo de ejemplo BT, Td, q, cm, Tj, Q, Tj, ET con una tabla de estados que muestra cómo Q deshace la escala de la CTM mientras el DX de 100 de la matriz de texto sobrevive a ambas pasadas de glifos
El cm dentro del par q/Q duplica la CTM solo para la primera serie de glifos, y Q elimina esa escala de nuevo. La matriz de texto establecida por Td sobrevive porque restaurar el estado la vuelve a aplicar en lugar de confiar en el registro gráfico extraído de la pila
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 PDF Library for Delphi 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

PDF Library for Delphi: cronología de recorte en dos pasos donde W solo registra la regla de recorte, el operador de dibujo de trayectoria siguiente intersecta los límites pendientes, y ClipBoundsExact se mantiene verdadero solo para un único rectángulo re
El recorte surte efecto cuando se ejecuta el siguiente operador de pintado de trazado, de modo que una instantánea previa tomada en W todavía reporta los límites antiguos, más amplios. Solo un re alineado a los ejes produce ClipBoundsExact en true, mientras que todo lo demás degrada honestamente a un límite exterior seguro

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. PDF Library for Delphi 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
  // Mismo índice de instrucción, dos instantes distintos: antes y después de ejecutarla
  Prog.GetGraphicsState(CmIndex, False, Before);
  Prog.GetGraphicsState(CmIndex, True, After);
  // Before.CTM refleja todos los cm anteriores; After.CTM ya incorpora
  // además la concatenación de esta misma instrucción
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 PDF Library for Delphi 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 PDF Library for Delphi. 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 PDF Library for Delphi, la biblioteca de componente PDF VCL nativa para Delphi y C++Builder