Artículo técnico

Renderizado de patrones de mosaico PDF en Delphi con HotPDF

El sombreado que se renderiza como un bloque gris plano es el fallo clásico de patrón de mosaico. HotPDF, el componente VCL nativo de PDF para 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 mosaico visible, con la selección de patrón mantenida en el estado gráfico y restaurada por q y Q

Los síntomas llegan en dos sabores, y se ven no 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 bloque de título que debería ser blanco liso adquiere las líneas diagonales de una vista de detalle dos rutas antes. Ambos son problemas de estado de patrón, y solo uno de ellos tiene que ver con dibujar mosaicos en absoluto

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

Porque el nombre de patrón seleccionado es parte del estado gráfico, no una propiedad del operador que lo usó. ISO 32000-1 §8.6.6.2 define un espacio de color Pattern como uno cuyo valor de color es un nombre de patrón suministrado a scn o SCN, y cada otro componente del estado de color es guardado por q y restaurado por Q. El nombre del patrón tiene que seguir la misma regla. HotPDF lo guarda 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 la CTM anterior

Guarda ese nombre en una variable local dentro del despachador de operadores en su lugar, y sobrevive a cada Q en el flujo. El fallo entonces aparece 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 empuja y saca estado que la variable perdida ignora. Establecer un espacio de color que no es patrón con cs o CS, o emitir un simple g / rg / k, también debe limpiar el nombre del patrón, de lo contrario la selección obsoleta sobrevive al espacio de color que le dio significado

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, 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. §8.7.3.2 describe un patrón de mosaico como un flujo de contenido replicado en intervalos horizontales y verticales fijos, y la replicación solo tiene sentido contra un recorte que ya tiene la forma correcta. Para rellenos la conversión es directa: HPDFSelectFillPathClip establece el modo de relleno del polígono a ALTERNATE para f*, B* y b* y a WINDING para las variantes de no cero, construye la ruta GDI, y la intersecta en el recorte con 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 en forma de dona

Los trazos son la parte fácil de hacer mal. Una ruta trazada no tiene interior, así que intersectar la ruta misma en el recorte produce una región vacía y nada se pinta. HPDFSelectStrokePathClip por lo tanto construye primero una pluma geométrica 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 un arreglo de guiones está activo, luego llama a WidenPath para convertir el contorno trazado en una región rellenable antes de recortar. El remate, la unión, el inglete y el comportamiento de guiones en una ruta trazada con patrón entonces coinciden con un trazo normal por construcción en vez de por una segunda implementación. Dos límites honestos viven aquí: los anchos de línea por debajo de una unidad de dispositivo se limitan a un píxel, y el arreglo de guiones se trunca a dieciséis entradas, que es el tope 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 cuadro de recorte del dispositivo, que está en espacio de dispositivo. HotPDF compone BaseMatrix := CTM * PatternMatrix, la invierte, y mapea las cuatro esquinas del cuadro 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 por XStep y YStep contra el BBox del patrón da rangos de índice cerrados. Cada celda luego se renderiza con una 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 más pequeño que el ancho del cuadro delimitador, que es cómo se expresan los diseños de mosaico superpuestos; 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 viene de afuera

Un patrón PaintType 2 lleva forma pero ningún color, y el color llega con el nombre del patrón. §8.7.3.2 especifica que un patrón sin color se usa solo con un espacio de color Pattern que declara un espacio subyacente, así que scn recibe primero los valores de componente y el nombre del patrón al final. HotPDF resuelve esos componentes a través del espacio subyacente almacenado en la entrada de espacio de color del patrón, lo que significa que un sombreado sin color puede teñirse con una tinta Separation o una combinación DeviceN exactamente como 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 agudamente. Para PaintType 2 el renderizador establece una bandera de supresión de operador de color durante la duración del 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, DeviceGray negro con un espacio de color identidad, y el mosaico se colorea a sí mismo. Saltarse ese restablecimiento deja que el color que resultaba estar activo en el operador f se filtre a un patrón que se suponía debía autodescribirse

¿Por qué debe restaurarse la profundidad de la pila de estado gráfico después de cada mosaico?

Porque a un flujo de contenido de patrón se le permite estar desbalanceado, y el daño se acumula a través de las 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 cualquier CTM y recorte que llevara ese cuadro. HotPDF por lo tanto toma una instantánea del registro de estado y 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 la bandera de supresión de operador 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 de 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 duros en vez de heurísticas. El anidamiento de patrones está limitado a profundidad 4, la misma protección usada para la recursión de XObject de formulario, que detiene un patrón que se referencia a sí mismo a través de su propio diccionario de recursos. Un solo pintado de ruta puede ejecutar como máximo 16,384 mosaicos en total, contados en descenso a través de patrones anidados y reiniciados solo cuando comienza el pintado del patrón más externo. Una cuadrícula de mosaicos cuyo conteo de celdas planificado excede lo que queda de ese presupuesto se rechaza de inmediato, antes de que se ejecute una sola celda

La geometría degenerada se rechaza en vez de aproximarse. Un BBox faltante o de área cero, un XStep o YStep cuya magnitud está por debajo de 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 hacen que el pintado del patrón retorne sin dibujar. El resultado es una región no pintada en vez de un hilo de renderizado colgado, que es el intercambio que quieres en un convertidor por lotes. El rendimiento viene de una decisión: el flujo del patrón se tokeniza una vez por pintado con HPDFTokenizeContentStream y el arreglo de tokens se reutiliza en cada celda visible, así que el conteo de mosaicos multiplica el costo de ejecución pero nunca el costo de análisis léxico

Renderizar una página con patrón desde Delphi

Nada sobre el soporte de patrones cambia el código que llama. Carga el documento, pide una página, y el trabajo de mosaico sucede 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, metaarchivo e impresora, así que un dibujo sombreado que se ve correcto en una miniatura de vista previa se imprime con la misma geometría de mosaico. Los patrones de sombreado PatternType 2 toman una rama diferente que comparte su ruta de evaluación con el operador sh desnudo, descrito en detalle bajo 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, verifica las tres clases de fallo en orden. Una región que está completamente en blanco generalmente significa un rechazo: inspecciona XStep, YStep y BBox en busca de valores degenerados, o cuenta los mosaicos que necesitaría la cuadrícula contra el tope de 16,384. Una región pintada en un solo color plano significa que el nombre del patrón nunca llegó al operador de pintado, lo que apunta al orden de cs y scn en el flujo. Un patrón que aparece donde no pertenece significa restauración de estado, y el lugar donde mirar es el manejo de q / Q alrededor del formulario o ruta que lo heredó

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