Artículo técnico

Renderizado de colores de separación y tintas planas DeviceN en Delphi

HotPDF renderiza colores de separación (Separation) y tintas planas DeviceN en páginas PDF cargadas resolviendo el espacio de color a través de HPDFResolveColorSpace, evaluando la función de transformación de matiz (tint-transform) con HPDFEvalTintTransform y convirtiendo el resultado a través del espacio alternativo a RGB para la pantalla. Desde la versión v2.375.0, esta línea de procesamiento evalúa los cuatro tipos de funciones PDF, incluidas las calculadoras PostScript de Tipo 4, de modo que un archivo listo para imprenta con tintas Pantone muestra sus colores reales en lugar de un marcador de posición. Este artículo explica cómo funciona la línea de procesamiento y, lo que es igual de útil, cómo falló mientras la construíamos

El caso que lo desencadena todo es siempre el mismo. Un cliente recibe un PDF de una imprenta: el titular está definido en una tinta plana con nombre, el diseño del embalaje utiliza una mezcla DeviceN de dos tintas y el texto del cuerpo es negro liso. En Acrobat se ve perfecto. En su aplicación Delphi, el titular se renderiza en negro, o peor aún, no se renderiza en absoluto, y el cliente notifica un error contra su software en lugar de contra el archivo. El archivo está bien. El renderizador simplemente no reconoce los espacios de color que utiliza el archivo

¿Por qué las tintas planas se renderizan en negro en un visor de PDF?

Las tintas planas se renderizan en negro, o desaparecen, cuando el renderizador implementa únicamente los operadores de color del dispositivo (rg, g, k) e ignora los genéricos. La norma ISO 32000-1 §8.6 define tres grupos de espacios de color: espacios de dispositivo (DeviceGray, DeviceRGB, DeviceCMYK), espacios basados en CIE (CalGray, CalRGB, Lab, ICCBased) y espacios especiales (Indexed, Separation, DeviceN, Pattern). Todo lo que queda fuera del grupo del dispositivo se selecciona con los operadores genéricos: cs y CS eligen un espacio por nombre del diccionario de recursos de la página, luego sc, SC, scn y SCN suministran los valores de los componentes. Un renderizador que omite esos operadores mantiene cualquier color que se haya establecido en último lugar, el cual, para una página que comienza con su titular en tinta plana, es el negro DeviceGray inicial

HotPDF añadió el juego de operadores genéricos a su renderizador de páginas en la versión v2.333.0, junto con una ruta de resolución unificada: cada entrada de recurso /ColorSpace, ya sea un nombre sin formato, una matriz insertada o una referencia indirecta, se analiza en un registro THPDFColorSpace, y cada solicitud de color de relleno o trazo se canaliza a través de una única llamada HPDFResolveColor. La enumeración de familias muestra la cobertura a simple vista

type
  THPDFColorSpaceFamily = (csfDeviceGray, csfDeviceRGB, csfDeviceCMYK,
                           csfIndexed, csfCalGray, csfCalRGB, csfLab,
                           csfICCBased, csfSeparation, csfDeviceN,
                           csfUnsupported);

function HPDFResolveColorSpace(Obj: THPDFObject): THPDFColorSpace;

function HPDFResolveColor(const CS: THPDFColorSpace;
  const Comps: THPDFColorComps; CompCount: Integer): THPDFRenderColor;

Una decisión de diseño demuestra su valor repetidamente: csfUnsupported es una familia de primera clase, no un error. Un espacio que el renderizador no puede interpretar se degrada a una alternativa definida en lugar de abortar la carga de la página, lo que coincide con el comportamiento de los visores convencionales y evita que un único relleno exótico impida mostrar un documento que por lo demás es renderizable

¿Cómo convierte una transformación de matiz un valor de tinta en un color real?

Un espacio de separación (Separation) contiene tres elementos de información: el nombre de la tinta, un espacio de color alternativo y una función de transformación de matiz (tint-transform). La matriz [/Separation /PANTONE485 /DeviceCMYK f] indica: cuando el flujo de contenido escribe 0.8 scn, introduzca el valor de matiz 0.8 en la función f y pinte el cuarteto CMYK resultante. DeviceN generaliza esto a N tintas con una función de N entradas. El nombre de la tinta en sí es solo indicativo en la pantalla; la transformación del matiz representa toda la semántica del renderizado, por lo que un renderizador que analiza el espacio pero omite la función aún no ha hecho nada útil

HPDFEvalTintTransform es el motor de funciones que respalda ese paso. Introducido en la versión v2.334.0, evalúa funciones exponenciales de Tipo 2 (C0 + x^N * (C1 - C0) con limitación de Domain y Range), funciones de combinación (stitching) de Tipo 3 (recursión de subfunción seleccionada por Bounds con reasignación Encode) y funciones muestreadas de Tipo 0 con muestras de 8, 16 y 32 bits. El Tipo 0 es el mismo mecanismo de tabla de búsqueda que cubrimos desde el punto de vista de la creación en el artículo sobre la creación de tablas de búsqueda de color Tipo 0; el lado del renderizado recorre la misma estructura de forma inversa, desde los bytes de muestra decodificados de vuelta a los valores de los componentes

Las funciones de calculadora PostScript de Tipo 4 eran el último bastión. Hasta la versión v2.375.0, se degradaban correctamente a un marcador de posición neutro; desde v2.375.0, HPDFEvalPostScriptCalculator ejecuta el conjunto completo de operadores de la norma ISO 32000-1 §7.10.5 en una pila de operandos limitada: operaciones aritméticas, de comparación, operadores booleanos y bit a bit, manipulación de la pila incluyendo roll, y condicionales if/ifelse. Las particularidades de su semántica son más estrictas de lo que parecen. La instrucción round de PostScript redondea el valor medio hacia el valor superior, por lo que no se puede utilizar la función de redondeo bancario Round de Delphi; los operadores trigonométricos funcionan en grados, con atan devolviendo valores en [0, 360); y exp es una potencia de dos operandos, no el exponencial natural. El mismo evaluador también dirige los rellenos de degradado basados en funciones, razón por la cual el renderizado de sombreado axial y radial incorporó rampas dirigidas por calculadora en la misma versión

// Evaluate a Separation/DeviceN tint transform (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
  const Inputs: THPDFColorComps; InputCount: Integer;
  out AltComps: THPDFColorComps): Boolean;

// Type 4 PostScript calculator, ISO 32000-1 7.10.5 operator set.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
  const Inputs: THPDFColorComps; InputCount: Integer;
  var Outputs: THPDFColorComps; OutCount: Integer): Boolean;

Honestidad sobre la precisión: una calculadora de software evaluada en doble precisión no coincidirá bit a bit con un procesador de imágenes tramadas (RIP), y la limitación en el límite de Range puede diferir en un bit menos significativo respecto a otra implementación. Para la visualización en pantalla y el renderizado de regresión, esto es irrelevante; si está construyendo un sistema de pruebas con gestión de color, la transformación del matiz es solo la primera etapa y seguirá necesitando un CMM real en fases posteriores

CalGray, CalRGB, Lab e ICCBased sin un motor ICC

Las familias basadas en CIE toman la otra rama del mismo solucionador. HotPDF convierte los valores Lab a través de la cadena estándar de Lab a XYZ y luego a sRGB, incluyendo el cubo de punto de ruptura 6/29 en la función de transferencia inversa, y gestiona CalRGB con su gamma por canal más una matriz lineal de 3x3 y CalGray con su gamma única. El detalle relevante para el rendimiento es el manejo del punto blanco: la matriz de conversión de XYZ a sRGB se adapta mediante el método Bradford al punto blanco declarado en el espacio de color y se almacena en caché en el registro THPDFColorSpace resuelto, por lo que el trabajo por píxel se reduce a una única multiplicación de 3x3, sin importar cuán exótico sea el iluminante declarado

Los espacios ICCBased reciben un tratamiento deliberadamente pragmático. La especificación PDF requiere que cada flujo ICCBased declare un espacio alternativo /Alternate o uno implícito a través de su recuento de componentes /N, precisamente para que los visores sin un motor de gestión de color puedan renderizar de forma coherente. HotPDF resuelve ICCBased a través de ese espacio alternativo, o deduce DeviceGray, DeviceRGB o DeviceCMYK a partir de un valor de /N de 1, 3 o 4 cuando la entrada está ausente, y nunca analiza los bytes del perfil. Esto significa que no hay dependencia de lcms ni coste de búsqueda de perfil, a cambio de la precisión colorimétrica: un espacio ICCBased cuyo perfil difiera notablemente de su alternativa mostrará la representación alternativa. Para la visualización en pantalla y las miniaturas, este es el mismo compromiso que asume cualquier visor ligero, y es el límite que debe indicar claramente en su propia documentación

El error que hacía fallar cada búsqueda de espacio de color con nombre

La canalización de operadores de la versión v2.333.0 se distribuyó con un defecto que permaneció invisible durante cuarenta y dos versiones: los controladores cs y CS buscaban su operando, con su barra inclinada inicial intacta (/CS0), frente a claves del diccionario de recursos almacenadas sin la barra (CS0). Cada búsqueda de espacio de color con nombre fallaba el cien por cien de las veces, y el código recurría silenciosamente al DeviceGray predeterminado. El síntoma visible era sutil de la peor manera: un relleno Separation de 1 scn se convertía en DeviceGray 1.0, que pinta de blanco, y la tinta blanca en una página blanca no es un error de renderizado del que nadie haga una captura de pantalla. La solución, en la versión v2.375.0, consistió en una función auxiliar de normalización de nombres compartida que se aplica en cada búsqueda de operando a recurso

Dos defectos hermanos surgieron de la misma investigación. En primer lugar, las referencias indirectas a objetos de tipo matriz se devolvían sin resolver: el acceso a documentos del renderizador tenía solucionadores tipados solo para flujos y diccionarios, por lo que /CS0 5 0 R apuntando a una matriz independiente [/Separation ...] se devolvía como la referencia directa y el espacio se analizaba como no compatible. En segundo lugar, HPDFReadNumericArray aplicaba una semántica de longitud estricta, lo que requería que la matriz PDF fuera al menos tan larga como el búfer suministrado. La lectura de las propiedades /C0 y /C1 de una función de Tipo 2 en un búfer de cuatro elementos fallaba para alternativas de uno y tres componentes, dejando ambas matrices en cero, y cada matiz exponencial que no fuera CMYK se renderizaba en negro hasta que la versión v2.376.0 introdujo el lector tolerante HPDFReadNumericArrayUpTo. La lección aplicable: cualquier clave PDF documentada que contenga una matriz numérica de longitud variable debe leerse con un lector que llene lo que exista, porque un búfer de tamaño fijo con coincidencia estricta convierte archivos válidos en ceros silenciosos

¿Cómo probar el renderizado de tintas planas sin engañarse a sí mismo?

La pregunta incómoda es por qué el conjunto de pruebas permaneció en verde durante todo este tiempo. La prueba de regresión original, SeparationRendersDistinguishable, solo afirmaba que el mapa de bits renderizado no era completamente negro. Una canalización que reducía cada tinta plana a DeviceGray producía una salida gris y blanca, que no es negra, por lo que la afirmación se cumplía mientras toda la característica estaba inactiva. Las afirmaciones débiles de la forma "no está en blanco", "no es todo negro" o "el resumen no es cero" no pueden distinguir un renderizador funcional de uno defectuoso, porque casi cualquier modo de fallo sigue produciendo algunos píxeles

El estilo de afirmación que realmente detecta estos fallos fija el color esperado: renderizar un PDF mínimo creado manualmente cuya tinta Separation se resuelva en un tono conocido y luego contar los píxeles en los que predomine ese tono. El renderizado en un mapa de bits para su inspección utiliza el mismo punto de entrada RenderLoadedPageToBitmap descrito en la guía de renderizado de página a mapa de bits

var
  Pdf: THotPDF;
  Bmp: TBitmap;
  X, Y, RedHits: Integer;
  Px: TColor;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('spot-red-fixture.pdf', '') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 96);
      try
        RedHits := 0;
        for Y := 0 to Bmp.Height - 1 do
          for X := 0 to Bmp.Width - 1 do
          begin
            Px := Bmp.Canvas.Pixels[X, Y];
            if (GetRValue(Px) > 180) and (GetGValue(Px) < 100) and
               (GetBValue(Px) < 100) then
              Inc(RedHits);
          end;
        // Fijar el color esperado: exigir un área real de píxeles
        // con predominio de rojo, nunca conformarse con "no todo negro".
        Assert(RedHits > 500);
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

El archivo de prueba (fixture) importa tanto como la afirmación. Un PDF creado a mano de unos pocos cientos de bytes, con un solo relleno Separation y nada más, no deja lugar a la ambigüedad sobre cuál es el resultado esperado; un archivo del mundo real ejecuta más código pero no puede indicarle qué etapa falló. Ahora consideramos el conteo de píxeles del color esperado como el nivel mínimo para cualquier prueba de renderizado rápida (smoke test), porque es el único estilo de afirmación que obligó a que salieran a la luz estos defectos

El renderizado de tintas planas y espacios de color CIE forma parte de la línea de procesamiento de documentos cargados en HotPDF Component para Delphi y C++Builder, junto con el motor de funciones, el renderizado de sombreado y las rutas de exportación de mapas de bits mostradas anteriormente