Artículo técnico

Gradientes PDF axiales y radiales en Delphi con PDFium

PDFium Component crea gradientes PDF axiales y radiales en Delphi a través de TPdf.SaveAsShadingCreated. Usted describe cada gradiente en un registro TPdfShadingCreate (tipo, espacio de color, coordenadas, colores o stops, página de destino), y el método escribe una copia verificada en la que cada relleno es un diccionario de shading real de ISO 32000-1, registrado en el diccionario /Resources /Shading de la página y pintado por el operador sh, así PDFium lo renderiza de forma nativa

La necesidad aparece en generadores de reportes y certificados. Un diseñador entrega un mock-up con una banda de encabezado suave de azul a blanco o un brillo radial detrás de un sello, y la API de objetos de página que cubre texto, trazados e imágenes no tiene nada para eso. Puede fingir un gradiente con cien rectángulos delgados en colores ligeramente distintos, pero el resultado hace bandas visibles al hacer zoom, infla el content stream, y se imprime mal. PDF tiene shadings suaves desde 1.3; el trabajo está en escribirlos de modo que un renderer realmente los dibuje

¿Por qué PDFium Component escribe los gradientes como un guardado aparte?

TPdf.SaveAsShadingCreated es una operación de guardado en lugar de una llamada de objetos de página porque el ABI público de PDFium no tiene ninguna función que cree un diccionario de shading o emita un operador sh. La capa de objetos de página que se usa al crear PDFs desde cero con PDFium Component construye trazados, texto e imágenes, y hasta ahí llega. Los shadings viven un nivel más abajo, en el grafo de objetos y la sintaxis del content stream

Así que el método trabaja por etapas. PDFium primero guarda el documento abierto de forma nativa, lo que conserva cada edición que usted hizo por la API normal. PDFium Component luego reescribe esa salida preparada: asigna un objeto indirecto por shading, agrega la entrada de recurso a cada página de destino, anexa un nuevo content stream al /Contents de la página, y escribe una tabla de referencias cruzadas fresca. Antes de que algo llegue a su stream o archivo, el resultado se recarga con una verificación estricta de referencias cruzadas, y el método devuelve False con un mensaje en el reporte si esa recarga falla. La sobrecarga de archivo escribe primero a un archivo temporal, así que un guardado fallido no machaca un destino existente

Construir un gradiente axial de dos colores

Un shading axial (ISO 32000-1 §8.7.4.5.3, Type 2) mezcla colores a lo largo de una línea de x0 y0 a x1 y1, así que Coords toma cuatro valores. Un shading radial (§8.7.4.5.4, Type 3) mezcla entre dos círculos y toma seis: x0 y0 r0 x1 y1 r1. Con StartColor y EndColor simples, PDFium Component emite una función exponencial Type 2 (§7.10.3) con /N 1, que es una interpolación lineal recta, y fija /Extend [true true] para que los colores finales continúen más allá de ambos extremos del eje

Geometría de shading axial y radial de PDFium Component para Delphi: un gradiente Type 2 mezcla StartColor a EndColor a lo largo de la línea de x0 y0 a x1 y1 con /Extend true true continuando más allá de ambos extremos, mientras un gradiente Type 3 mezcla entre un círculo inicial de radio r0 y un círculo final de radio r1
PDFium pinta el shading sobre lo que el clip permita: los Coords Type 2 colocan el eje de mezcla de x0 y0 a x1 y1, los Coords Type 3 colocan los dos círculos, y /Extend [true true] lleva ambos colores finales más allá de la geometría
uses
  System.SysUtils, PDFium, FPdfCompress;

procedure AddHeaderBand(const InFile, OutFile: string);
var
  Pdf: TPdf;
  Fills: TPdfShadingCreateShadings;
  Report: TPdfShadingCreateReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InFile;
    Pdf.Active := True;

    SetLength(Fills, 1);
    Fills[0] := Default(TPdfShadingCreate);
    Fills[0].ShadingType := pstAxial;
    Fills[0].ColorSpace := pscDeviceRGB;
    Fills[0].Coords := [0, 792, 0, 640];        // x0 y0 x1 y1, de arriba hacia abajo
    Fills[0].StartColor := [0.12, 0.35, 0.75];  // azul oscuro en el borde superior
    Fills[0].EndColor := [1, 1, 1];             // blanco más abajo
    Fills[0].PageNumber := 1;                   // base 1
    Fills[0].ResourceName := 'HeaderBand';      // opcional, el valor por defecto es Sh1, Sh2...

    if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
      raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
  finally
    Pdf.Free;
  end;
end;

El conteo de componentes de cada color debe coincidir con el espacio de color: uno para pscDeviceGray y pscCalGray, tres para pscDeviceRGB, pscCalRGB y pscLab, cuatro para pscDeviceCMYK. Para pscICCBased el conteo viene de los bytes del perfil que usted pasa en IccProfile, que debe exponer 1, 3 o 4 componentes; el perfil se incrusta como su propio stream ICC con el /N correspondiente. Los espacios calibrados aceptan CalWhitePoint opcional (tres valores, Y positivo), CalGamma (un valor para CalGray, tres para CalRGB) y, para Lab, un LabRange de seis valores. Cada coordenada y componente se verifica contra NaN e infinito antes de escribir un byte

Note que las coordenadas están en el espacio de usuario por defecto, con el origen en la parte inferior izquierda de la página. Recuerde que el eje solo define dónde ocurre la mezcla: el área pintada es lo que el clip permita, lo que importa más de lo que suena, como muestran las siguientes secciones

Los gradientes multi-stop se compilan a una stitching function Type 3

PDFium Component convierte un arreglo Stops en la función que un lector PDF espera: dos stops se vuelven una única función Type 2, y de 3 a 256 stops se vuelven una stitching function Type 3 (ISO 32000-1 §7.10.4) hecha de un segmento Type 2 lineal por par adyacente. Cada TPdfShadingCreateStop lleva un Position y un Color. Cuando Stops no está vacío reemplaza por completo a StartColor y EndColor

La stitching function necesita tres arreglos que a mano es fácil errar sutilmente. /Functions contiene los segmentos, /Bounds contiene solo las posiciones de stops interiores (k segmentos necesitan k-1 bounds), y /Encode mapea cada subdominio de vuelta a [0 1] para que cada segmento vea su rango de entrada completo. El componente deriva los tres de sus stops, razón por la que es estricto con la entrada: las posiciones deben ser finitas, estrictamente crecientes, empezar en 0 y terminar en 1 (dentro de 1e-6). Una lista de stops que empiece en 0.1 dejaría parte del dominio sin función definida, y el método la rechaza en lugar de adivinar

function Stop(Position: Double; const Rgb: array of Double): TPdfShadingCreateStop;
var
  I: Integer;
begin
  Result.Position := Position;
  SetLength(Result.Color, Length(Rgb));
  for I := 0 to High(Rgb) do
    Result.Color[I] := Rgb[I];
end;

procedure BuildSealGlow(var Fill: TPdfShadingCreate);
begin
  Fill := Default(TPdfShadingCreate);
  Fill.ShadingType := pstRadial;
  Fill.ColorSpace := pscDeviceRGB;
  // x0 y0 r0 x1 y1 r1: un punto en el centro del sello creciendo a un círculo de 90 pt
  Fill.Coords := [450, 160, 0, 450, 160, 90];
  Fill.Stops := [Stop(0.0, [1.00, 0.95, 0.70]),
                 Stop(0.4, [0.95, 0.75, 0.30]),
                 Stop(1.0, [1.00, 1.00, 1.00])];
  Fill.PageNumber := 1;
end;

Para un gradiente de tres stops eso produce /Functions con dos segmentos Type 2, /Bounds [0.4] y /Encode [0 1 0 1]. Las fronteras de segmento son uniones de color duras, así que una lista de stops no suaviza a través de ellas; si una unión se ve muy filosa, agregue stops en lugar de esperar que el lector interpole curvatura

Compilación de gradiente multi-stop de PDFium Component en Delphi: tres Stops en posiciones 0.0, 0.4 y 1.0 se vuelven una stitching function Type 3 con un segmento Type 2 por par de stops adyacentes, /Bounds [0.4] para la posición interior y /Encode [0 1 0 1] mapeando cada subdominio de vuelta al rango completo
Cada par de stops adyacentes se vuelve un segmento Type 2 dentro de una función Type 3; los arreglos /Bounds y /Encode derivados son lo que las stitching functions escritas a mano suelen errar

¿Por qué PDFium no dibuja nada en silencio?

PDFium renderiza un shading recién creado solo cuando dos detalles están bien: /ShadingType debe ser un entero, y la llamada sh debe quedar dentro de un trazado de recorte. Equivoque cualquiera de los dos y el archivo abre, valida estructuralmente, y muestra una página intacta. Sin error, sin aviso, sin línea de log

Trampa uno: /ShadingType es un número, no un nombre

ISO 32000-1 Tabla 78 define /ShadingType como un entero: 2 para axial, 3 para radial. La primera versión de este escritor emitía /ShadingType /Axial porque se leía mejor en un volcado hex. PDFium lee la clave con un getter de entero, obtiene 0 para un nombre, clasifica el shading como inválido, falla al cargarlo, y se salta el operador sh sin quejarse. La página se renderizaba en blanco y nada en PDFium decía por qué. La causa solo se hizo visible tras construir un PDF mínimo a mano y abrirlo en un segundo renderer independiente, que reportaba un tipo de shading desconocido de 0. La lección se generaliza a todo escritor de PDFs: cuando la especificación tipa una enumeración como entero, escriba el entero, por más tentador que se vea un nombre autodescriptivo

Trampa dos: un content stream que solo contiene sh no tiene bounding box

El operador sh (ISO 32000-1 §8.7.4.2) pinta el shading sobre la región de recorte actual, así que la extensión de la propia página lo acota en papel. PDFium, sin embargo, construye un objeto de página de shading cuyo rectángulo viene del trazado de clip activo o del bounding box acumulado por operadores de dibujo anteriores en el mismo stream, y el renderer descarta los objetos de rectángulo vacío. Un stream recién anexado que no contiene nada más que /Sh1 sh no tiene ninguno de los dos, así que el gradiente se descarta antes de la rasterización. TPdf.SaveAsShadingCreated por tanto lee el crop box de cada página (relegando al media box, luego al tamaño de página) y envuelve cada invocación en un clip explícito re W n. En una página US Letter con su origen en cero, el stream anexado se ve así:

q
0 0 612 792 re W n
/HeaderBand sh
Q

El par q/Q aísla el clip y cualquier transformación del contenido existente de la página, así que nada de lo dibujado después en la página los hereda. Si escribe invocaciones de shading por su cuenta en otra herramienta, copie esta forma: un clip explícito en el mismo stream es barato y elimina la dependencia de lo que viniera antes

Por qué un shading PDF de aspecto válido no dibuja nada en PDFium: un /ShadingType escrito como el nombre Axial lo lee un getter de entero como 0 y el operador sh se salta en silencio, y un content stream que solo contiene sh no tiene bounding box hasta que un clip explícito re W n le da un rectángulo
Ambas fallas de shading invisible son silenciosas en PDFium, así que SaveAsShadingCreated escribe el ShadingType entero por su cuenta y envuelve cada llamada sh en un clip re W n del crop box

¿Cómo interactúa una matriz de gradiente personalizada con el clip?

Una matriz de gradiente en PDFium Component se aplica como un operador cm colocado después del clip y antes de sh, así que el clip sigue siendo de página completa mientras solo la geometría del gradiente se transforma. Fije HasMatrix a True y llene MatrixA a MatrixF, los mismos seis números de cualquier matriz de transformación de PDF. Los valores deben ser finitos y el determinante distinto de cero, porque una matriz singular colapsaría el shading sobre una línea. El componente también registra los seis valores como /Matrix en el diccionario de shading, pero para una llamada sh directa es el cm del content stream el que fija el sistema de coordenadas en el que PDFium pinta

El orden importa aquí, y es la tercera trampa. Los operandos de re se interpretan en la matriz de transformación actual. Ponga el cm primero y el rectángulo de clip del tamaño de la página se escala, rota o sesga junto con el gradiente, así que una matriz que aplasta una elipse también aplasta el área visible y partes de la página pierden su relleno. Recortar primero en espacio de usuario sin transformar, y luego concatenar la matriz, da el resultado esperado: un gradiente transformado cubriendo toda la página. La guía de sellos Form XObject y su matriz de seis números recorre lo que hace cada componente de la matriz si necesita construir una

procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
  BuildSealGlow(Fill);       // relleno radial centrado en 450,160
  Fill.Coords := [0, 0, 0, 0, 0, 90];  // círculos en el origen
  Fill.HasMatrix := True;
  Fill.MatrixA := 1.6;       // estirar horizontalmente
  Fill.MatrixB := 0;
  Fill.MatrixC := 0;
  Fill.MatrixD := 0.8;       // comprimir verticalmente
  Fill.MatrixE := 450;       // luego mover al centro del sello
  Fill.MatrixF := 160;
end;

Leer el reporte y conocer los límites

TPdfShadingCreateReport le dice qué se escribió: SourceByteCount y OutputByteCount, CreatedShadingObjectNumbers (un número de objeto por entrada, en orden), AttachedShadingCount, Succeeded y ErrorMessage. Los errores de validación nombran la entrada y el campo, por ejemplo una lista de stops que no es estrictamente creciente, así que valen registrarse verbatim

if Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
  Writeln(Format('%d shading(s) attached, %d -> %d bytes',
    [Report.AttachedShadingCount, Report.SourceByteCount,
     Report.OutputByteCount]))
else
  Writeln('Rejected: ' + Report.ErrorMessage);

El perfil es deliberadamente acotado. Crea solo shadings axiales y radiales; los shadings basados en función y de malla (Types 1 y 4 a 7) y los espacios de color de patrón se rechazan. Una llamada acepta de 1 a 4096 rellenos, los números de página deben existir, y los nombres de recurso se limitan a 127 caracteres de letras, dígitos, guion bajo, guion y punto. La entrada cifrada se rechaza en la etapa de trailer. Un documento firmado necesita AllowSignedDocument := True, y como la salida es una reescritura completa en lugar de una actualización incremental, las firmas existentes ya no verificarán, así que firme después de agregar gradientes. Cada relleno se pinta al final del contenido de la página, encima de lo que ya está; para un efecto de fondo, cree primero el gradiente y agregue texto e imágenes después, o use optional content como se describe en el artículo sobre controlar capas de optional content de PDF en Delphi cuando el relleno deba poder conmutarse

FAQ

¿SaveAsShadingCreated soporta gradientes multi-stop?

Sí. Pase de 2 a 256 entradas en Stops con posiciones estrictamente crecientes de 0 a 1. Dos stops producen una función Type 2 y tres o más producen una stitching function Type 3 con arreglos /Bounds y /Encode derivados

¿Por qué mi gradiente es invisible en PDFium cuando el archivo parece válido?

Verifique que /ShadingType sea el entero 2 o 3 en lugar de un nombre, y que el operador sh corra dentro de un clip re W n en el mismo content stream. PDFium se salta en silencio los tipos de shading inválidos y descarta los objetos de shading de rectángulo vacío

¿Qué espacios de color puede usar un shading creado?

TPdfShadingCreate acepta DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab e ICCBased con un perfil de 1, 3 o 4 componentes. Los espacios de color Pattern e Indexed no son parte del perfil de creación

La creación de gradientes, el optional content y el resto de los escritores por etapas vienen con el PDFium Component for Delphi y C++Builder, que envuelve PDFium para proyectos Delphi, C++Builder y Lazarus