Artículo técnico

Renderizado de patrones en mosaico PDF en Delphi con HotPDF

Un sombreado que se renderiza como un único bloque gris plano es el fallo clásico de los patrones en mosaico. HotPDF, el componente VCL nativo para PDF en Delphi y C++Builder, pinta PatternType 1 convirtiendo la ruta actual en un recorte temporal y reproduciendo el flujo de contenido del patrón una vez por cada mosaico visible, con la selección de patrón retenida en el estado gráfico y restaurada por q y Q

Los síntomas llegan en dos variantes, y parecen no estar relacionados hasta que conoces la causa. Un dibujo CAD pierde su sombreado de sección y vuelve como rellenos sólidos, porque el renderizador resolvió el patrón a un color promedio y pintó eso. O el sombreado se escapa: un cajetín de título que debería ser blanco liso recoge las líneas diagonales de una vista de detalle dos rutas antes. Ambos son problemas de estado del patrón, y solo uno de ellos tiene realmente que ver con dibujar mosaicos

¿Por qué un patrón en mosaico se filtra a la siguiente ruta?

Porque el nombre de patrón seleccionado forma parte del estado gráfico, no una propiedad del operador que lo usó. La norma ISO 32000-1 §8.6.6.2 define un espacio de color Pattern como aquel cuyo valor de color es un nombre de patrón suministrado a scn o SCN, y todos los demás componentes del estado de color son guardados por q y restaurados por Q. El nombre de patrón tiene que seguir la misma regla. HotPDF lo mantiene en el registro de estado como FillPatternName y StrokePatternName, junto con la familia de espacio de color de relleno y trazo, así que un Q restablece la selección anterior exactamente igual que restablece el CTM anterior

Guarda ese nombre en una variable local dentro del despachador de operadores en su lugar, y sobrevivirá a todos los Q del flujo. El fallo aparece entonces en algún lugar inesperado: un XObject de formulario dibujado después de la ruta con patrón hereda una selección de patrón que su propio flujo de contenido nunca hizo, y sus rellenos salen sombreados. Los formularios anidados lo empeoran, porque cada nivel de anidamiento apila y desapila estado que la variable descarriada ignora. Fijar un espacio de color que no sea de patrón con cs o CS, o emitir un simple g / rg / k, también debe borrar el nombre de patrón, o si no la selección obsoleta sobrevive al espacio de color que le dio sentido

q
  /Pattern cs              % pattern colour space, ISO 32000-1 8.6.6.2
  /P1 scn                  % coloured tiling pattern, PaintType 1
  10 10 200 120 re f       % this rectangle is hatched
Q
0 0 300 200 re f           % must be black again, not hatched

q
  /Cs2 cs                  % [/Pattern /DeviceCMYK] array
  0 0.6 1 0 /P2 scn        % uncoloured pattern plus its underlying colour
  20 20 160 90 re f*
Q

Un patrón se pinta a través de un recorte, nunca como un relleno

El modelo correcto es sustractivo: restringir el recorte del dispositivo a la forma que se está pintando, y luego ejecutar el contenido del patrón dentro de él. HotPDF nunca dibuja primero una aproximación sólida y la sobrepinta, porque el sólido intermedio sería visible a través de los huecos entre mosaicos y competiría con cualquier transparencia en el contenido del mosaico. El §8.7.3.2 describe un patrón en mosaico como un flujo de contenido replicado a intervalos horizontales y verticales fijos, y la replicación solo tiene sentido contra un recorte que ya tenga la forma correcta. Para los rellenos la conversión es directa: HPDFSelectFillPathClip fija el modo de relleno de polígono a ALTERNATE para f*, B* y b* y a WINDING para las variantes de regla no-cero, construye la ruta GDI, y la interseca con el recorte mediante SelectClipPath. Esa única línea es lo que hace que un relleno con patrón par-impar deje los mismos huecos que un relleno sólido par-impar, que es exactamente lo que necesita una región sombreada con forma de rosquilla

Los trazos son la parte fácil de hacer mal. Una ruta trazada no tiene interior, así que intersecar la propia ruta con el recorte produce una región vacía y no se pinta nada. HPDFSelectStrokePathClip por tanto construye primero un pincel geométrico a partir del estado actual, usando PS_GEOMETRIC con el remate de J, la unión de j, el límite de inglete de M, y PS_USERSTYLE cuando hay un array de guiones activo, y luego llama a WidenPath para convertir el contorno trazado en una región rellenable antes de recortar. El comportamiento del remate, la unión, el inglete y los guiones en una ruta trazada con patrón coincide entonces con un trazo normal por construcción y no por una segunda implementación. Aquí conviven dos límites honestos: los anchos de línea por debajo de una unidad de dispositivo se limitan a un píxel, y el array de guiones se trunca a dieciséis entradas, que es el techo que acepta ExtCreatePen

¿Qué mosaicos son realmente visibles?

El rango visible viene de ejecutar la transformación hacia atrás. La colocación de mosaicos ocurre en el espacio del patrón, pero lo único que sabe cuánto de la página se está tocando es el rectángulo de recorte del dispositivo, que está en espacio de dispositivo. HotPDF compone BaseMatrix := CTM * PatternMatrix, la invierte, y mapea las cuatro esquinas del rectángulo de recorte GDI de vuelta a través de la inversa. Los límites alineados con los ejes de esas cuatro esquinas mapeadas dan el rectángulo en espacio de patrón que posiblemente puede cubrirse, y dividir ese rectángulo entre XStep e YStep contra el BBox del patrón da rangos de índice cerrados. Cada celda se renderiza entonces con un CTM de CTM * PatternMatrix * Translate(i * XStep, j * YStep), y se recorta una segunda vez a su propio polígono BBox transformado. Ese segundo recorte importa cuando XStep es menor que el ancho del rectángulo delimitador, que es cómo se expresan los diseños de mosaico solapados; sin él, las celdas vecinas se pintarían unas sobre otras fuera de su extensión declarada. Si el recorte por celda vuelve como NULLREGION, la celda se salta sin tokenizar ni ejecutar nada

// Map the device clip box back into pattern space through the inverse of
// CTM * PatternMatrix, then convert those bounds into tile index ranges.
BaseMatrix := HPDFMatMul(FGSStack.State.CTM, PatternMatrix);
if not HPDFMatInvert(BaseMatrix, InverseMatrix) then Exit;   // singular: refuse
if GetClipBox(FDC, ClipRect) = ERROR then Exit;

// MinX..MaxY are the axis-aligned bounds of the four mapped clip corners.
I0 := Floor((MinX - BBox[2]) / StepXAbs);
I1 := Ceil ((MaxX - BBox[0]) / StepXAbs);
J0 := Floor((MinY - BBox[3]) / StepYAbs);
J1 := Ceil ((MaxY - BBox[1]) / StepYAbs);

PlannedTiles := Int64(I1 - I0 + 1) * Int64(J1 - J0 + 1);
if (PlannedTiles <= 0) or (PlannedTiles > FPatternTilesRemaining) then Exit;
Dec(FPatternTilesRemaining, Integer(PlannedTiles));

Patrones sin color y el color que llega desde fuera

Un patrón PaintType 2 lleva forma pero no color, y el color llega con el nombre de patrón. El §8.7.3.2 especifica que un patrón sin color solo se usa con un espacio de color Pattern que declara un espacio subyacente, así que scn recibe primero los valores de componente y el nombre de patrón al final. HotPDF resuelve esos componentes a través del espacio subyacente almacenado en la entrada del espacio de color del patrón, lo que significa que un sombreado sin color puede tintarse con una tinta Separation o una combinación DeviceN exactamente igual que cualquier otro relleno; la mecánica de esa resolución se cubre en renderizar colores directos Separation y DeviceN. Dentro del mosaico, los dos tipos de pintura divergen bruscamente. Para PaintType 2, el renderizador fija un indicador de supresión de operadores de color durante todo el mosaico, así que cualquier g, rg, k o scn en el contenido del patrón se ignora y cada marca toma el color suministrado externamente. Para PaintType 1 se aplica lo contrario: el estado de relleno y trazo se restablece a los valores por defecto de PDF, negro DeviceGray con un espacio de color identidad, y el mosaico se colorea a sí mismo. Saltarse ese restablecimiento deja que el color que resultara ser el actual en el operador f se filtre a un patrón que se suponía que era autodescriptivo

¿Por qué hay que restaurar la profundidad de la pila de estado gráfico tras cada mosaico?

Porque a un flujo de contenido de patrón se le permite estar desequilibrado, y el daño se acumula entre celdas. Un mosaico cuyo flujo contiene tres operadores q y dos operadores Q deja la pila un cuadro más profunda de lo que empezó. Restaura solo el registro de estado actual entre celdas y la profundidad sigue creciendo, así que la celda número doscientos se ejecuta desde un cuadro de pila que pertenece a la celda número ciento noventa y nueve, con el CTM y el recorte que llevara ese cuadro. HotPDF por tanto toma una instantánea del registro de estado y de la profundidad de la pila antes del bucle de mosaicos y llama a RestoreSnapshot al comienzo de cada iteración, lo que trunca la pila de vuelta a la longitud guardada y reinstala el estado guardado en un solo paso. El diccionario de recursos de la página y el indicador de supresión de operadores de color se restauran en el mismo límite, ya que un mosaico puede referenciar sus propios recursos y no debe entregárselos a su vecino. El estado de recorte GDI recibe el mismo tratamiento a través de un par SaveDC / RestoreDC alrededor de cada celda, así que un mosaico que instala su propio recorte W n no puede reducir la región disponible para el siguiente

Presupuestos, rechazos y lo que el renderizador no dibujará

Los patrones en mosaico son el lugar más fácil en un PDF para escribir un archivo de denegación de servicio, así que los límites son números fijos en lugar de heurísticas. El anidamiento de patrones está limitado a una profundidad de 4, la misma protección usada para la recursión de XObjects de formulario, que detiene un patrón que se referencia a sí mismo a través de su propio diccionario de recursos. Una sola pintura de ruta puede ejecutar como máximo 16.384 mosaicos en total, contados hacia abajo a través de patrones anidados y reiniciados solo cuando empieza la pintura del patrón más externo. Una rejilla de mosaico cuyo recuento de celdas planificado supera lo que queda de ese presupuesto se rechaza directamente, antes de que se ejecute una sola celda

La geometría degenerada se rechaza en lugar de aproximarse. Un BBox ausente o de área cero, un XStep o YStep cuya magnitud sea inferior a 1e-6, un producto CTM * PatternMatrix sin inversa, coordenadas de recorte mapeadas más allá de 1e9, o una magnitud de índice más allá de un millón, todo ello hace que la pintura del patrón vuelva sin dibujar. El resultado es una región sin pintar en lugar de un hilo de renderizado colgado, que es la compensación que quieres en un convertidor por lotes. El rendimiento proviene de una decisión: el flujo del patrón se tokeniza una vez por pintura con HPDFTokenizeContentStream y el array de tokens se reutiliza en cada celda visible, así que el número de mosaicos multiplica el coste de ejecución pero nunca el coste de análisis léxico

Renderizar una página con patrón desde Delphi

Nada en el soporte de patrones cambia el código llamante. Carga el documento, pide una página, y el trabajo de mosaico ocurre dentro del intérprete de flujo de contenido que ya impulsa el renderizado de página a bitmap. El mismo intérprete alimenta contextos de dispositivo de bitmap, metarchivo e impresora, así que un dibujo sombreado que se ve bien en una miniatura de vista previa se imprime con la misma geometría de mosaico. Los patrones de sombreado PatternType 2 toman una rama distinta que comparte su ruta de evaluación con el operador sh desnudo, descrito en detalle en renderizado de sombreados axiales y radiales

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('assembly-drawing.pdf') > 0 then
    begin
      // Section hatching that previously flattened to a solid block now
      // replays the tile content once per visible cell.
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 200);
      if Assigned(Bmp) then
      try
        Bmp.SaveToFile('sheet1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Cuando una región con patrón todavía se ve mal, comprueba las tres clases de fallo en orden. Una región completamente en blanco normalmente significa un rechazo: inspecciona XStep, YStep y BBox en busca de valores degenerados, o cuenta los mosaicos que necesitaría la rejilla contra el techo de 16.384. Una región pintada en un único color plano significa que el nombre de patrón nunca llegó al operador de pintura, lo que apunta al orden de cs y scn en el flujo. Un patrón que aparece donde no debería significa un problema de restauración de estado, y el lugar donde mirar es el manejo de q / Q alrededor del formulario o la ruta que lo heredó

Los patrones en mosaico son una de esas funciones de PDF que permanecen invisibles hasta que el archivo que las necesita llega a tu bandeja de entrada, y entonces son todo el trabajo. Si estás construyendo visores de dibujos, convertidores de documentos de ingeniería o generadores de informes en Delphi o C++Builder, el componente completo y su API de renderizado están documentados en la página de componente PDF para Delphi HotPDF