HotPDF renderiza colores especiales (spot colors) Separation y 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 mediante el espacio alternativo a RGB para la pantalla. Desde la versión v2.375.0, esa canalización evalúa los cuatro tipos de funciones de PDF, incluidas las calculadoras PostScript Tipo 4, por lo 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 canalización y, de manera igualmente útil, cómo fallaba mientras la construíamos
El caso que lo desencadena es siempre el mismo. Un cliente recibe un PDF de una imprenta: el titular está configurado en una tinta especial con nombre, el diseño del empaque utiliza una mezcla de dos tintas DeviceN y el texto del cuerpo es negro simple. 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 registra un error contra su software en lugar del archivo. El archivo está bien. El renderizador simplemente no reconoce los espacios de color que utiliza el archivo
¿Por qué los colores especiales se renderizan en negro en un visor de PDF?
Los colores especiales 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 está fuera del grupo de dispositivos se selecciona con los operadores genéricos: cs y CS eligen un espacio por nombre en el diccionario de recursos de la página, luego sc, SC, scn, and SCN suministran los valores de los componentes. Un renderizador que omite esos operadores conserva el último color que se configuró, lo que para una página que comienza con su titular en color especial representa el negro DeviceGray inicial
HotPDF añadió el conjunto 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 simple, una matriz integrada 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 primera 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 página, lo que coincide con el comportamiento de los visores convencionales y evita que un único relleno exótico deje en blanco un documento que de otro modo sería renderizable
¿Cómo convierte una transformación de matiz un valor de tinta en un color real?
Un espacio 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. La matriz [/Separation /PANTONE485 /DeviceCMYK f] indica: cuando el flujo de contenido escribe 0.8 scn, envíe el valor de matiz 0.8 a la función f y dibuje el cuádruple CMYK resultante. DeviceN generaliza esto a N tintas con una función de N entradas. El nombre de la tinta en sí es solo informativo en pantalla; la transformación de matiz representa toda la semántica de renderizado, por lo que un renderizador que analiza el espacio pero omite la función no ha realizado nada útil aún
HPDFEvalTintTransform es el motor de funciones detrás de ese paso. Incorporado en la versión v2.334.0, evalúa funciones exponenciales Tipo 2 (C0 + x^N * (C1 - C0) con límites de Domain y Range), funciones de imposición (stitching) Tipo 3 (recursión de subfunción seleccionada por Bounds con remapeo de Encode) y funciones muestreadas Tipo 0 con muestras de 8, 16 y 32 bits. El Tipo 0 representa el mismo mecanismo de tabla de búsqueda que analizamos desde la perspectiva de creación en el artículo sobre la construcción de tablas de búsqueda de color Tipo 0; el lado de renderizado recorre la misma estructura en orden inverso, desde los bytes de muestra decodificados hasta los valores de los componentes
Las funciones de calculadora PostScript Tipo 4 eran las últimas pendientes de admitir. Hasta la versión v2.375.0, se degradaban de manera controlada a un marcador de posición neutro; desde la 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: aritmética, comparación, operadores booleanos y a nivel de bits, manipulación de la pila (incluido roll) y condicionales if/ifelse. Las semánticas de los casos extremos son más estrictas de lo que parecen. El operador round de PostScript redondea la mitad hacia el valor mayor, por lo que no se puede utilizar el método de redondeo bancario Round de Delphi; los operadores trigonométricos funcionan en grados, y atan devuelve valores en [0, 360); y exp es una potencia de dos operandos, no el exponencial natural. El mismo evaluador también gestiona los rellenos degradados basados en funciones, por lo que el renderizado de sombreado axial y radial incorporó rampas basadas en calculadoras 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;
Sinceridad sobre la precisión: una calculadora de software evaluada en doble precisión no coincidirá bit a bit con un RIP, y la delimitació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 eso es irrelevante; si está construyendo pruebas con gestión de color, la transformación de matiz es solo la primera etapa y seguirá necesitando un CMM real posteriormente
CalGray, CalRGB, Lab e ICCBased sin un motor ICC
HotPDF convierte valores Lab a través de la cadena estándar de Lab a XYZ a sRGB, incluyendo la aproximación cúbica con punto de división 6/29 en la función de transferencia inversa, y maneja CalRGB con su gamma por canal más una matriz lineal de 3x3 y CalGray con su gamma única. El detalle importante para el rendimiento es el manejo del punto blanco: la matriz de conversión de XYZ a sRGB se adapta mediante el método de 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 sigue siendo una única multiplicación de 3x3, sin importar qué tan exótico sea el iluminante declarado
Los espacios ICCBased reciben un tratamiento deliberadamente pragmático. La especificación PDF requiere cada flujo ICCBased declare un espacio /Alternate o uno implícito a través de su número 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 esa alternativa, o asume DeviceGray, DeviceRGB o DeviceCMYK según /N sea 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 perfiles, a cambio de la precisión colorimétrica: un espacio ICCBased cuyo perfil difiera notablemente de su alternativa mostrará la renderización alternativa. Para la visualización en pantalla y las miniaturas, este es el mismo acuerdo que realiza cualquier visor ligero, y es el límite que debe declarar claramente en su propia documentación
El error que hacía fallar cada búsqueda de espacio de color con nombre
La estructura 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 inicial intacta (/CS0), frente a claves de 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 a la opción predeterminada DeviceGray. El síntoma visible era sutil de la peor manera: un relleno de Separation de 1 scn se convertía en DeviceGray 1.0, lo que dibuja en blanco, y dibujar tinta blanca sobre una página blanca no es un error de renderizado que alguien registre en una captura de pantalla. La solución, en la v2.375.0, fue un asistente de normalización de nombres compartido 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 con valor de matriz se devolvían sin resolver: el acceso al documento del renderizador tenía resolvedores tipados únicamente para flujos y diccionarios, por lo que /CS0 5 0 R que apuntaba a una matriz independiente [/Separation ...] se devolvía como la referencia directa y el espacio se analizaba como unsupported. En segundo lugar, HPDFReadNumericArray aplicaba semánticas de longitud estrictas, exigiendo que la matriz PDF fuera al menos tan larga como el búfer suministrado. La lectura de los valores /C0 y /C1 de una función Tipo 2 en un búfer de cuatro elementos fallaba entonces para alternativas de uno y tres componentes, dejando ambas matrices en cero, y cada matiz exponencial no CMYK se renderizaba en negro hasta que la versión v2.376.0 introdujo el lector flexible HPDFReadNumericArrayUpTo. La lección aplicable: cualquier clave PDF documentada como contenedora de una matriz numérica de longitud variable debe leerse con un lector que llene lo que exista, ya que un búfer de tamaño fijo con coincidencia estricta convierte archivos válidos en ceros silenciosos
¿Cómo probar el renderizado de colores especiales sin engañarse a sí mismo?
La pregunta incómoda es por qué el conjunto de pruebas se mantuvo en verde durante todo esto. La regresión original, SeparationRendersDistinguishable, solo afirmaba que el mapa de bits renderizado no era completamente negro. Una canalización que reducía cada color especial a DeviceGray producía un resultado en gris y blanco, lo cual no es negro, por lo que la afirmación se cumplía mientras que toda la característica estaba inoperativa. Las afirmaciones débiles de tipo "no está en blanco", "no es todo negro" o "el resumen no es cero" no pueden distinguir un renderizador funcional de uno dañado, ya que casi cualquier modo de fallo sigue produciendo algunos píxeles
El estilo de afirmación que realmente detecta estos fallos bloquea el color esperado: renderice un PDF mínimo creado manualmente cuya tinta Separation se resuelva en una tonalidad conocida, luego cuente los píxeles predominantes en esa tonalidad. La renderización a un mapa de bits para 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;
// Lock the expected ink: demand a real area of red-dominant
// pixels, never settle for "not all black".
Assert(RedHits > 500);
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
El recurso de prueba (fixture) es tan importante como la afirmación. A PDF creado manualmente de unos pocos cientos de bytes, con un solo relleno de Separation y nada más, no deja dudas 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 rápida (smoke test) de renderizado, debido a que es el único estilo de afirmación que obligó a revelar estos defectos
El renderizado de colores especiales y espacios de color CIE forma parte de la canalización 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 a mapa de bits mostradas anteriormente