Artículo técnico

Funciones PDF tipo 2/3/4 en Delphi: exponencial, PostScript

HotPDF, el componente PDF VCL nativo para Delphi y C++Builder, evalúa los tres tipos de función PDF construidos a partir de fórmulas en lugar de cuadrículas de muestras: interpolación exponencial tipo 2, empalme (stitching) tipo 3, y funciones de calculadora PostScript tipo 4, correspondientes a ISO 32000-1 §7.10.3, §7.10.4 y §7.10.5. El tipo 2 mezcla entre dos vectores de salida a lo largo de una curva, el tipo 3 encadena varias subfunciones a través de un único dominio de entrada, y el tipo 4 ejecuta un programa PostScript restringido que puede bifurcar, comparar y calcular casi cualquier cosa que un flujo de contenido necesite a partir de sus entradas. Si cualquiera de los tres se implementa ligeramente mal, la falla nunca se anuncia como un bug: aparece como un degradado con una banda completamente plana, un color directo que se renderiza en negro puro, o una función de calculadora que se desvía por exactamente uno en las entradas que una suite de pruebas no llegó a probar

Estos tres se ubican junto a un cuarto, el tipo 0, que almacena una cuadrícula muestreada en lugar de una fórmula y se cubre por separado en el artículo complementario sobre tablas de búsqueda de color tipo 0. Las dos familias resuelven el mismo problema, mapear una entrada a una salida, pero el tipo 0 son datos calculados una vez e incrustados en el archivo, mientras que los tipos 2, 3 y 4 son código que el lector evalúa en cada llamada. Los cuatro comparten un único punto de despacho en el renderizador de HotPDF, indexado por la entrada /FunctionType del diccionario de función, así que un sombreado, una transformación de tinte, o una función de punto de medio tono nunca necesitan saber cuál de los cuatro recibieron antes de poder pedir un color

¿Cómo funciona una función PDF exponencial tipo 2?

Una función PDF tipo 2 calcula una única fórmula —y = C0 + x^N × (C1 − C0), aplicada componente por componente— donde x es la única entrada de la función, normalizada respecto a su /Domain antes de que la fórmula se ejecute (ISO 32000-1 §7.10.3). /C0 y /C1 son los vectores de salida en los dos extremos de ese rango, un número por componente de salida, y /N es el exponente que da forma a la curva entre ellos: N = 1 da la rampa lineal recta detrás de la mayoría de las paradas de degradado y conversiones a dos tonos, N mayor que 1 tira la curva hacia C0, y N entre 0 y 1 la empuja hacia C1. RegisterExponentialFunction construye ese diccionario a partir de cinco argumentos y entrega un objeto función listo para conectarse a un sombreado, una función de punto de medio tono, o cualquier otro lugar donde la especificación acepte una clave /Function

La relación de conteo de componentes entre C0 y C1 importa dos veces: una al crear una función tipo 2, y otra vez cada vez que HotPDF tiene que renderizar una que no creó. Del lado de la creación, RegisterExponentialFunction comprueba C0 y C1 entre sí y lanza una excepción si no coinciden, así que una llamada que llega a BeginDoc ya tiene un objeto función internamente consistente. Del lado del renderizado, sin embargo, el evaluador tiene que confiar en cualesquiera arreglos /C0 y /C1 que un archivo de origen realmente declare —un archivo de imprenta abierto para vista previa, digamos, o un documento firmado mostrado de vuelta a un usuario— y las versiones anteriores a la 2.376.0 leían esos arreglos en un búfer dimensionado para cuatro componentes, el caso CMYK. Un tinte exponencial DeviceGray o DeviceRGB, con un /C0 y /C1 de uno o tres elementos, fallaba esa lectura silenciosamente y dejaba ambos arreglos en cero, así que el tinte se pintaba negro plano en lugar de su color previsto. La versión 2.376.0 redimensionó el lector al conteo de salida realmente declarado por la función en lugar de un búfer fijo —exactamente el tipo de bug que solo un caso de prueba que no sea CMYK expone, ya que la suite existente se ejecutaba en CMYK en todo momento, donde cuatro-en-cuatro siempre encajaba

var
  EaseIn: THPDFDictionaryObject;
begin
  // Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
  EaseIn := Pdf.RegisterExponentialFunction(
    [0,1],           // Domain: single input, clamped to [0,1]
    [0, 0, 0],       // C0: output at x = 0
    [0.8, 0, 0],     // C1: output at x = 1
    3,               // N: exponent, 1 = linear, > 1 eases toward C0
    []);             // Range omitted: defaults to a [0,1] clamp per output
end;

Empalme tipo 3: encadenar subfunciones a través de un arreglo Bounds

Una función PDF tipo 3 empalma k subfunciones en un único mapeo por tramos sobre el /Domain de una sola entrada, y los dos arreglos que hacen que esto funcione son /Bounds y /Encode (ISO 32000-1 §7.10.4). /Bounds contiene k − 1 puntos de división interiores que dividen /Domain en k intervalos consecutivos; el evaluador elige el primer intervalo cuyo límite superior exceda la entrada, o el último intervalo una vez que la entrada alcanza el límite final, y transfiere el control a la subfunción de ese intervalo. /Encode luego remapea la entrada, desde su posición dentro de ese intervalo, hacia el rango de entrada que la subfunción elegida realmente espera —típicamente [0, 1] si la subfunción es un segmento exponencial más— antes de que la evaluación continúe, una llamada más profundo, dentro del propio /Domain y /Range de esa subfunción

El evaluador de empalme de HotPDF solía manejar solo exactamente dos subfunciones, y su lector de /Bounds requería un arreglo completo de ocho elementos, así que el único punto de división que un degradado de dos segmentos realmente necesita —un número en /Bounds— siempre fallaba al analizarse y la función no devolvía nada. /Encode no se aplicaba en absoluto. La versión 2.376.0 reescribió la selección como la búsqueda general de k subfunciones que describe la especificación y empezó a leer /Bounds según su longitud real declarada, así que un degradado de tres, cuatro o cinco paradas empalmado a partir de esa cantidad de segmentos exponenciales ahora se resuelve de la misma manera en que uno de dos segmentos siempre afirmó hacerlo. El ejemplo de abajo construye una rampa de dos segmentos de negro a rojo a blanco, la forma a la que un sombreado axial o radial recurre cada vez que una sola curva exponencial no puede llevar todas las paradas de color que exige un diseño

var
  ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
  // Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
  ToRed   := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0],   [0.8, 0, 0], 1, []);
  ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1],   1, []);

  Ramp := Pdf.RegisterStitchingFunction(
    [0,1],                  // Domain: the stitched function's own input range
    [ToRed, ToWhite],       // Functions: k = 2 sub-functions
    [0.5],                  // Bounds: k - 1 = 1 split point
    [0,1, 0,1],             // Encode: 2 numbers per sub-function
    []);                    // Range omitted: inherited from each sub-function
end;

¿Qué puede hacer una función de calculadora PostScript tipo 4 que los tipos 2 y 3 no pueden?

Una función PDF tipo 4 ejecuta un programa genuino, aunque deliberadamente restringido: una calculadora PostScript que empuja sus entradas a una pila de operandos, ejecuta operadores aritméticos, de comparación, de manipulación de pila y booleanos, más condicionales if/ifelse, y deja sus salidas en la pila al terminar (ISO 32000-1 §7.10.5, Tabla 42). No hay estructura de bucle ni almacenamiento de variables con nombre, solo la pila, lo que mantiene un programa conforme fácil de razonar —pero dentro de ese conjunto restringido de operadores, el tipo 4 puede expresar cosas que los tipos 2 y 3 no pueden, como una fórmula real de mezcla multi-tinta para una separación DeviceN o una función de punto de medio tono con un umbral condicional. El evaluador de HotPDF, HPDFEvalPostScriptCalculator, tokeniza el programa una vez —números, operadores, y bloques de procedimiento { }— y luego recorre una pila de operandos de 100 entradas, la profundidad que exige ISO 32000-1 §7.10.5, detrás de un tope estricto de 50.000 operadores evaluados como respaldo defensivo contra programas patológicos o escritos a mano

El operador roll: la dirección es fácil de invertir por error

roll es el operador que con más probabilidad sale invertido en un primer intento, porque tanto el orden de sus argumentos como su dirección de rotación van en contra de cómo se describirían en español coloquial. n j roll extrae de la pila un conteo n y una cantidad de rotación j, y luego desplaza cíclicamente las n entradas superiores de la pila en j posiciones, envolviendo los elementos que caen de un extremo de vuelta al otro; el ejemplo canónico, directamente de la especificación, es a b c 3 1 roll produciendo c a b —el elemento superior se mueve al fondo del grupo, no al revés, y cada otro elemento se desplaza una posición hacia arriba para hacer espacio. El evaluador de HotPDF calcula la nueva posición de la entrada de pila i como (i + j) mod n, que coincide exactamente con ese ejemplo, pero es un bucle de dos líneas igual de fácil de escribir con la rotación invertida, y un roll reflejado igual produce un color que parece plausible —simplemente no es el color que pidió el autor del archivo

const
  Prog = '{ 3 1 roll }';   // (a b c) -> (c a b): the third input moves to the front
var
  Reorder: THPDFStreamObject;
begin
  // Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
  Reorder := Pdf.RegisterPostScriptFunction(
    [0,1, 0,1, 0,1],   // Domain: 2 numbers per input
    [0,1, 0,1, 0,1],   // Range: 2 numbers per output (required for Type 4)
    Prog);
end;

round no es el Round de Delphi: redondeo hacia arriba en .5 versus redondeo bancario

El operador round de PostScript resuelve un empate en .5 hacia el entero mayor siempre, y la función Round integrada de Delphi no lo hace: redondea la mitad al par (half-to-even), la convención de redondeo bancario que alterna hacia qué lado cae un empate en .5 para que el redondeo repetido no acumule sesgo. Las dos coinciden casi en todas partes y difieren exactamente en el límite que aquí importa —el Round(0.5) de Delphi devuelve 0 y Round(2.5) devuelve 2, mientras que el round de la especificación de PDF quiere 1 y 3 para esas mismas entradas— así que el desajuste se esconde en pruebas casuales y luego se reproduce como un error consistente de más/menos uno en cualquier lugar donde la aritmética intermedia de un programa de calculadora caiga exactamente en un medio entero. ISO 32000-1 §7.10.5 Tabla 42 es explícita en que round empuja una fracción .5 hacia el entero mayor, así que HotPDF implementa el operador como Floor(x + 0.5) en lugar de llamar al Round de Delphi, y cualquier código que reimplemente o verifique manualmente la aritmética de un programa tipo 4 necesita la misma sustitución

function PostScriptRound(const X: Double): Double;
begin
  // ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
  // integer. Delphi's Round() is banker's rounding and disagrees here:
  // Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
  Result := Floor(X + 0.5);
end;

La validación en tiempo de registro atrapa temprano un programa de calculadora defectuoso

Un programa tipo 4 malformado es barato de atrapar en el momento de la creación y costoso de atrapar en cualquier otro lugar, así que RegisterPostScriptFunction no se limita a almacenar el texto fuente: evalúa el programa a modo de prueba una vez, en el punto medio del /Domain declarado, antes de que el objeto función se escriba jamás en el documento. Bloques { } desbalanceados, un operador no reconocido, un desbordamiento por defecto de pila, o un conteo de salida que no coincide con /Range, todos fallan esa ejecución de prueba y lanzan una excepción de inmediato, con la pila de llamadas apuntando a la llamada a RegisterPostScriptFunction en lugar de a un artefacto de renderizado descubierto durante el control de calidad de un archivo que ya salió a producción. La prueba en el punto medio no demuestra que el programa sea correcto en todo su /Domain —una rama condicional que solo se comporta mal cerca de un borde del rango de entrada todavía puede pasar inadvertida por un único punto de muestra— pero cierra toda la clase de programas que están estructuralmente rotos en lugar de simplemente equivocados en un rincón

Dónde ponen a trabajar estas funciones los degradados y los colores directos

Los tipos 2, 3 y 4 rara vez aparecen aislados en un PDF real; aparecen dondequiera que la especificación acepte una clave /Function, y los dos consumidores más comunes son los sombreados y las transformaciones de tinte de color directo. El operador sh de un degradado axial o radial (ISO 32000-1 §8.7.4.5) evalúa su /Function una vez por posición a lo largo del eje del degradado, que es exactamente el caso de múltiples paradas para el que existe el empalme tipo 3. La transformación de tinte de un espacio de color Separation o DeviceN es el otro hogar frecuente de estos tres tipos, y es donde el tipo 4 se gana su lugar: una sola tinta directa normalmente se reduce a una curva tipo 2 o tipo 0, pero una mezcla DeviceN de varias tintas con comportamiento real de trapping y sobreimpresión a menudo necesita la lógica condicional que solo una calculadora PostScript puede expresar, el caso cubierto en el artículo sobre el renderizado de colores directos Separation y DeviceN. RegisterSeparationFunc es la llamada de emparejamiento del lado de la creación: toma un nombre de colorante, un espacio de color alternativo, y cualquier objeto que devuelva la familia Register*Function, y conecta esa transformación de tinte a un recurso de espacio de color Separation que el resto de la página puede seleccionar con scn/SCN

Juntas, las cuadrículas muestreadas del tipo 0 y estos tres tipos guiados por fórmula cubren cada /Function que un PDF puede declarar, y elegir el correcto es sobre todo una cuestión de qué ya se tiene: una tabla de búsqueda calculada en otro lugar se convierte en tipo 0, una mezcla de dos extremos se convierte en tipo 2, varias mezclas encadenadas a través de un dominio se convierten en tipo 3, y cualquier cosa con lógica condicional real se convierte en tipo 4. RegisterExponentialFunction, RegisterStitchingFunction, y RegisterPostScriptFunction son parte del componente HotPDF estándar para Delphi y C++Builder, junto con el resto de su API de funciones y sombreados de ISO 32000-1