Artículo técnico

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

HotPDF, el componente PDF nativo de VCL para Delphi y C++Builder, evalúa los tres tipos de función PDF construidos a partir de fórmulas en lugar de rejillas de muestras: la interpolación exponencial de Tipo 2, el empalme de Tipo 3 y las funciones de calculadora PostScript de Tipo 4, correspondientes a ISO 32000-1 §7.10.3, §7.10.4 y §7.10.5. El Tipo 2 mezcla 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 capaz de ramificarse, comparar y calcular casi cualquier cosa que un flujo de contenido necesite a partir de sus entradas. Si os equivocáis ligeramente en cualquiera de los tres, el fallo nunca se anuncia como un error: aparece como un degradado con una banda plana muerta, un color plano que se renderiza en negro puro, o una función de calculadora que se desvía exactamente en uno en las entradas que una batería de pruebas resultó no probar

Estos tres conviven con un cuarto, el Tipo 0, que almacena una rejilla muestreada en lugar de una fórmula y se cubre por separado en el artículo complementario sobre las tablas de búsqueda de color de Tipo 0. Las dos familias resuelven el mismo problema, asociar una entrada a una salida, pero el Tipo 0 son datos calculados una vez e incorporados al 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, de modo que un sombreado, una transformación de tinte o una función de punto de trama nunca necesitan saber cuál de los cuatro han recibido antes de poder pedir un color

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

Una función PDF de Tipo 2 calcula una única fórmula, y = C0 + x^N × (C1 − C0), aplicada componente a componente, donde x es la única entrada de la función, normalizada según su /Domain antes de que se ejecute la fórmula (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 ambos: N = 1 produce la rampa lineal recta que hay detrás de la mayoría de las paradas de degradado y las conversiones a duotono, N por encima de 1 empuja la curva hacia C0, y N entre 0 y 1 la empuja hacia C1. RegisterExponentialFunction construye ese diccionario a partir de cinco argumentos y devuelve un objeto de función listo para conectarse a un sombreado, a una función de punto de trama o a cualquier otro lugar donde la especificación admita una clave /Function

La relación de número de componentes entre C0 y C1 importa en dos momentos: al crear una función de Tipo 2, y de nuevo cada vez que HotPDF tiene que renderizar una que no creó él mismo. Del lado de la creación, RegisterExponentialFunction comprueba C0 y C1 entre sí y lanza una excepción si discrepan, así que una llamada que llega a BeginDoc ya contiene un objeto de función internamente coherente. Del lado del renderizado, sin embargo, el evaluador tiene que confiar en cualesquiera arrays /C0 y /C1 que un archivo de origen realmente declare, un archivo de imprenta abierto para vista previa, por ejemplo, o un documento firmado que se muestra de vuelta a un usuario, y las versiones anteriores a 2.376.0 leían esos arrays en un búfer dimensionado para cuatro componentes, el caso CMYK. Un tinte exponencial DeviceGray o DeviceRGB, con un /C0 y un /C1 de uno o tres elementos, fallaba esa lectura en silencio y dejaba ambos arrays a cero, así que el tinte se pintaba en negro plano en lugar de con el color previsto. La versión 2.376.0 redimensionó el lector al número real de salidas declarado por la función en lugar de un búfer fijo, exactamente el tipo de fallo que solo un caso de prueba que no sea CMYK deja al descubierto, ya que la batería existente ejecutaba CMYK en todos los casos, donde cuatro contra 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 de Tipo 3: encadenar subfunciones a través de un array Bounds

Una función PDF de Tipo 3 empalma k subfunciones en una única asociación por tramos sobre el /Domain de una sola entrada, y los dos arrays 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 supere la entrada, o el último intervalo una vez que la entrada alcanza el límite final, y le pasa el control a la subfunción de ese intervalo. /Encode entonces vuelve a mapear la entrada, desde su posición dentro de ese intervalo, hacia cualquiera que sea el rango de entrada que la subfunción elegida espera en realidad, 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 adentro, en el propio /Domain y /Range de esa subfunción

El evaluador de empalme de HotPDF solía gestionar solo exactamente dos subfunciones, y su lector de /Bounds exigía un array 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 otros tantos segmentos exponenciales ahora se resuelve del mismo modo que un degradado de dos segmentos siempre afirmó hacerlo. El ejemplo de abajo construye una rampa de dos segmentos de negro a rojo y de rojo a blanco, la forma a la que recurre un sombreado axial o radial siempre que una única 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 de Tipo 4 que los Tipos 2 y 3 no puedan?

Una función PDF de Tipo 4 ejecuta un programa genuino, aunque deliberadamente restringido: una calculadora PostScript que apila sus entradas en una pila de operandos, ejecuta operadores aritméticos, de comparación, de manipulación de pila y booleanos, además de condicionales if/ifelse, y deja sus salidas en la pila al terminar (ISO 32000-1 §7.10.5, Tabla 42). No hay ninguna construcción de bucle ni almacenamiento de variables con nombre, solo la pila, lo que mantiene fácil de razonar un programa conforme, pero dentro de ese conjunto de operadores restringido, el Tipo 4 puede expresar cosas que el Tipo 2 y el Tipo 3 no pueden, como una auténtica fórmula de mezcla multitinta para una separación DeviceN o una función de punto de trama con un umbral condicional. El evaluador de HotPDF, HPDFEvalPostScriptCalculator, tokeniza el programa una sola 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, con un tope máximo de 50.000 operadores evaluados como salvaguarda defensiva frente a 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 intuitivamente en español. n j roll desapila un contador n y una cantidad de rotación j, y a continuación desplaza cíclicamente las n entradas superiores de la pila en j posiciones, haciendo que los elementos que caen por un extremo reaparezcan por el otro; el ejemplo canónico, directamente de la especificación, es a b c 3 1 roll, que produce c a b, el elemento superior se mueve al fondo del grupo, no al revés, y cada uno de los demás elementos se desplaza una posición hacia arriba para dejarle sitio. 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 sigue produciendo un color de aspecto plausible, sencillamente no es el color que el autor del archivo pidió

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 frente a redondeo bancario

El operador round de PostScript resuelve un empate en .5 redondeando siempre hacia el entero mayor, y la función integrada Round de Delphi no lo hace: redondea al par más cercano, la convención de redondeo bancario que alterna hacia qué lado cae un empate en .5 para que el redondeo repetido no acumule sesgo. Ambas coinciden casi en todas partes y discrepan 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 PDF quiere 1 y 3 para esas mismas entradas, así que el desajuste se esconde durante unas pruebas superficiales y luego se reproduce como un desvío constante de uno allí donde la aritmética intermedia de un programa de calculadora cae exactamente en un semientero. La Tabla 42 de ISO 32000-1 §7.10.5 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 compruebe a mano la aritmética de un programa de 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 al registrar detecta pronto un programa de calculadora defectuoso

Un programa de Tipo 4 mal formado sale barato de detectar en el momento de la creación y caro de detectar en cualquier otro sitio, así que RegisterPostScriptFunction no se limita a almacenar el texto fuente: evalúa el programa una vez de prueba, en el punto medio del /Domain declarado, antes de que el objeto de función se escriba jamás en el documento. Bloques { } desequilibrados, un operador no reconocido, un vaciado de pila por defecto o un número de salidas que no coincide con /Range hacen fallar esa ejecución de prueba y lanzan una excepción de inmediato, con la pila de llamadas señalando hacia la llamada a RegisterPostScriptFunction en lugar de hacia un artefacto de renderizado descubierto durante el control de calidad de un archivo ya publicado. 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 extremo del rango de entrada aún puede colarse por delante de un único punto de muestra, pero sí descarta 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 planos

Los Tipos 2, 3 y 4 rara vez aparecen aislados en un PDF real; aparecen en cualquier lugar donde la especificación admite una clave /Function, y los dos consumidores más habituales son los sombreados y las transformaciones de tinte de color plano. El operador sh de un degradado axial o radial (ISO 32000-1 §8.7.4.5) evalúa su /Function una vez por cada posición a lo largo del eje del degradado, que es exactamente el caso de múltiples paradas para el que existe el empalme de 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 el sueldo: una única tinta plana normalmente se reduce a una curva de Tipo 2 o de 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 planos Separation y DeviceN. RegisterSeparationFunc es la llamada complementaria del lado de la creación: recibe un nombre de tinta, 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

Juntos, las rejillas muestreadas del Tipo 0 y estos tres tipos gobernados por fórmulas cubren todas las /Function que un PDF puede declarar, y elegir el adecuado es sobre todo una cuestión de qué tenéis ya: una tabla de búsqueda calculada en otro sitio 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 forman parte del componente HotPDF estándar para Delphi y C++Builder, junto con el resto de su API de funciones y sombreados conforme a ISO 32000-1