Artículo técnico

Blend mode en grupos y máscaras de luminosidad en PDF

HotPDF renderiza un grupo de transparencia PDF pintado con un blend mode distinto de Normal dejando que el grupo calcule su propia fusión: Cb(1 − α) + α·B(Cb, Cs), usando el alfa del grupo que rastrea o recupera de una pasada sobre negro y otra sobre blanco. Desde la v2.770.0, la propiedad THotPDF.RenderColorimetricLuminosityMasks también elige si las soft masks de luminosidad leen el color como RGB ponderado, como los visores comunes, o como CIE Y, como describe ISO 32000-1

Los dos cambios salieron del mismo tipo de archivo: una página exportada desde una herramienta de diseño, donde la foto de un producto se apoya en una sombra suave. La sombra es un Form XObject dibujado como grupo de transparencia con /BM /Multiply, y su desvanecimiento es una soft mask de luminosidad. En otros visores la sombra es un gris claro y neutro. En builds antiguos de HotPDF salía casi negra y teñida hacia el color del fondo. Dos bugs independientes estaban apilados uno encima del otro, y merece la pena desmontarlos porque cualquier renderer que crezca desde un diseño de "objetos opacos primero" se topará con ambos

¿Por qué una sombra Multiply se renderiza demasiado oscura?

Una sombra Multiply se renderiza demasiado oscura cuando el renderer fusiona el resultado compuesto del grupo en lugar del color y el alfa propios del grupo. Los píxeles parcialmente transparentes de ese resultado ya contienen el fondo, así que fusionarlos con el fondo multiplica el fondo por sí mismo. La fusión Normal con una opacidad es lineal y sobrevive a este error sin que se note; Multiply, Screen, Darken y todos los demás modos no lineales no

El pipeline de render de HotPDF manejaba originalmente los blend modes por objeto pintado con un modelo simple y rápido. Antes de pintar un objeto, el renderer fotografía los píxeles afectados; tras pintar, compara los bitmaps "antes" y "después" y trata el color "después" como una fuente opaca Cs, calculando B(Cb, Cs) y componiéndola con la opacidad actual. Para un path relleno, un glifo o una imagen opaca eso es exactamente correcto: la fuente de verdad es opaca dentro de su forma, y la forma es donde cambiaron los píxeles

Un grupo de transparencia rompe esa suposición. ISO 32000-1 §11.4 define un grupo como algo que primero se compone en su propio resultado, con su propio color y su propio alfa, y solo entonces se fusiona con el fondo como un objeto único. Donde una sombra es opaca al 30 por ciento, el bitmap "después" guarda aproximadamente 0.3 del color de la sombra más 0.7 de la página que hay debajo. Alimentas eso a Multiply como fuente opaca y la fórmula se convierte en fondo por (un valor que ya contiene el fondo): la sombra se oscurece y toma el tono del fondo. Nada revienta, ningún diagnóstico salta, y la página simplemente se ve mal, que es la peor forma de fallar para un renderer

Cómo HotPDF deja que un grupo de transparencia fusione por su cuenta

Desde la v2.769.3, HotPDF saca la fusión de un grupo pintado bajo un blend mode distinto de Normal del camino genérico por objeto y la mete en el propio renderer del grupo. El grupo calcula Cb(1 − α) + α·B(Cb, Cs) directamente desde su color, su alfa y la fotografía del fondo tomada para el operador Do, y luego marca el resultado como ya fusionado para que el Do envolvente solo lo componga con la opacidad actual en lugar de fusionar una segunda vez. Los objetos simples conservan el modelo rápido de antes-y-después, y los grupos bajo fusión Normal no se ven afectados porque el caso lineal nunca estuvo mal

La parte difícil es el alfa. Un grupo aislado (/I true) parte de un fondo totalmente transparente, así que el renderer ya rastrea un plano de alfa separado para él y puede leer α directamente. Un grupo no aislado se compone sobre lo que ya haya en la página y no tiene plano de alfa propio. HotPDF lo recupera dibujando el grupo dos veces, una sobre negro y otra sobre blanco:

  • Sobre negro, cada píxel acaba siendo α·Cs
  • Sobre blanco, el mismo píxel acaba siendo α·Cs + (1 − α)
  • Restando sale α = 1 − (white − black) y luego Cs = black / α

Esto funciona porque cada composición Normal que el renderer realiza, incluida la AlphaBlend de GDI para imágenes con soft masks y bordes con anti-aliasing, es lineal en el fondo. El truco tiene una precondición, eso sí: el contenido del grupo debe fusionarse en Normal. Si los recursos ExtGState propios del grupo fijan un /BM distinto de Normal, las dos pasadas ya no difieren en un término lineal limpio, así que HotPDF no parte el grupo y recurre al comportamiento anterior. La comprobación cubre solo los recursos del propio grupo, no los forms anidados. El coste también es real: esas páginas se renderizan algo más despacio porque el grupo se dibuja dos veces. Las soft masks construidas dentro del grupo se conservan hasta que ambas pasadas terminan, así que la segunda pasada pega del cache de máscaras en lugar de reconstruirlas

// Ilustración de la recuperación de alfa por canal (valores 0..1).
// Black = grupo dibujado sobre fondo negro, White = sobre fondo blanco.
procedure RecoverGroupPixel(Black, White: Double;
  out Alpha, Source: Double);
begin
  Alpha := 1.0 - (White - Black);
  if Alpha < 0.0 then Alpha := 0.0;
  if Alpha > 1.0 then Alpha := 1.0;
  if Alpha > 0.0 then
    Source := Black / Alpha   // deshacer la premultiplicación
  else
    Source := 0.0;            // totalmente transparente: el color da igual
end;

// Blend Multiply, luego la composición del grupo Cb(1 - a) + a * B(Cb, Cs)
function CompositeMultiply(Backdrop, Source, Alpha: Double): Double;
begin
  Result := Backdrop * (1.0 - Alpha) + Alpha * (Backdrop * Source);
end;

El código de arriba es una ilustración de las matemáticas, no una API de HotPDF; el renderer hace lo mismo por canal y por píxel dentro de THotPDF.RenderLoadedPageToBitmap. En la práctica, el alfa recuperado de dos pasadas de 8 bits pierde precisión al acercarse a cero, así que los bordes muy tenues llevan algo de ruido de cuantización, que es por lo que la división solo tiene sentido donde el alfa está claramente por encima de cero

El blend mode se reinicia a Normal dentro del grupo

El blend mode, igual que el alfa constante y la soft mask, se aplica al grupo de transparencia como un todo y no a cada objeto dentro de él, así que el contenido del grupo debe partir de fusión Normal (ISO 32000-1 §11.6.6). HotPDF reiniciaba antes la opacidad y la soft mask al entrar en el grupo pero dejaba el blend mode como estuviera. El resultado dentro de un grupo aislado era dramático: el contenido se multiplicaba a sí mismo contra el fondo inicial vacío del grupo, que es negro, y todo el grupo se pintaba negro

El mismo reinicio aplica cuando se construye una soft mask. Si un ExtGState fija a la vez /BM /Multiply y /SMask, el grupo de la máscara debe dibujarse con fusión Normal; si no, un grupo de máscara blanco se vuelve negro y esconde justo lo que la máscara debía revelar. Ambos reinicios son parte de la v2.769.3, y ambos son invisibles en documentos simples, que es como sobrevivieron tanto tiempo

¿Qué valor debería tomar una soft mask de luminosidad para un gris medio?

Bajo ISO 32000-1, una soft mask de luminosidad cuyo grupo usa un espacio de fusión basado en CIE debería tomar CIE 1931 Y, y la Y de un gris medio sRGB de 0.5 ronda solo el 0.21, porque Y es luz lineal. Los visores PDF comunes en cambio resuelven el color del grupo a RGB y ponderan los valores codificados más o menos 0.30 / 0.59 / 0.11, lo que da unos 0.5 para el mismo gris. Desde la v2.770.0 HotPDF usa por defecto la ponderación compatible con visores y ofrece THotPDF.RenderColorimetricLuminosityMasks para la lectura de la especificación

El hueco no es académico. Un desvanecimiento en una exportación de herramienta de diseño suele ser un degradado en un grupo ICCBased o calibrado, y los tonos medios son justo donde la Y lineal y el gris con gamma divergen más. Con CIE Y, una máscara de gris medio deja pasar solo cerca de un quinto de la sombra en lugar de la mitad, así que las sombras proyectadas y las viñetas salen notablemente más claras que en cualquier otro visor con el que el cliente abra el archivo. Tener razón por la letra de la norma y estar mal ante los ojos de todos es un mal intercambio para un valor por defecto, así que el por defecto sigue a los visores y el modo estricto está a una propiedad de distancia

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('catalog-page.pdf') > 0 then
    begin
      // Por defecto (False): las máscaras de luminosidad ponderan el color
      // resuelto como DeviceRGB, igual que lo que muestran los visores comunes.
      // True: tomar CIE Y cuando el grupo de la máscara tiene espacio basado en CIE.
      Pdf.RenderColorimetricLuminosityMasks := False;
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 150);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('catalog-page-viewer.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

La propiedad solo importa cuando el espacio de fusión del grupo de la máscara está basado en CIE: CalGray, CalRGB, Lab o ICCBased. Las máscaras cuyo grupo usa DeviceGray, DeviceRGB o DeviceCMYK se renderizan exactamente igual que antes bajo cualquiera de los dos ajustes, ya que la conversión del espacio de dispositivo a gris es la misma en ambos modos

Trampas: /BC, /CS y el cache de render

Una soft mask de luminosidad es tan consistente como su fondo: el color /BC que rellena el área de la máscara fuera del grupo debe convertirse con la misma regla que la pintura dentro del grupo. HotPDF cambió ambas cosas juntas en la v2.770.0. Convertir la pintura con RGB ponderado pero el fondo con CIE Y produce una costura visible donde termina el bounding box del grupo, un bug fácil de introducir cuando el fondo pasa por un helper distinto del contenido

Los atributos del grupo además tienen que ser válidos antes de que todo esto ocurra. HotPDF lee el espacio de fusión de la entrada /CS del grupo y exige que /BC lleve exactamente un número por componente de ese espacio: tres para un espacio basado en RGB, cuatro para CMYK, uno para gris. Una /CS ausente o un /BC de aridad equivocada hace que la máscara falle antes de que el grupo se ejecute, y la pintura se dibuja entonces sin máscara en lugar de enmascararse con un fondo inventado. Separation, DeviceN y NChannel no son espacios de fusión legales, así que la pintura de tinta directa dentro de un grupo de máscara pasa por su transformación de tinte al espacio alternativo antes de calcular la luminosidad

Por último, el ajuste cambia píxeles, así que forma parte de la clave de cache. Los bitmaps renderizados con RenderColorimetricLuminosityMasks activado y desactivado se cachean por separado tanto en el cache de render de memoria como en el de disco, y alternar la propiedad entre llamadas a THotPDF.RenderLoadedPageToBitmapCached nunca devuelve un bitmap del otro modo. Una nota operativa: la v2.769.3 introdujo una regresión solo en Win64 por la que las páginas con un grupo de transparencia que no fijaba soft mask propia fallaban al renderizarse; está corregida en la v2.770.1, así que las aplicaciones de 64 bits deberían usar esa release o posterior

// Prueba en paralelo: renderiza la misma página en ambos modos.
// Cada modo se cachea bajo su propia clave, así que las llamadas repetidas son baratas.
Pdf.RenderColorimetricLuminosityMasks := False;
ViewerBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);

Pdf.RenderColorimetricLuminosityMasks := True;
StrictBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);

// ... muestra ambos y libéralos: las llamadas cacheadas devuelven copias del caller
ViewerBmp.Free;
StrictBmp.Free;

Para la API de render más amplia, incluida la elección de DPI, el cache de páginas y lo que el renderer todavía aproxima, véase renderizar una página PDF a un TBitmap de Delphi con HotPDF. Si tu problema va en dirección contraria, quitar transparencia de un archivo para imprenta o archivado en lugar de dibujarla, soft masks y aplanado de transparencia en PDFlibPas cubre ese lado

FAQ

¿Renderiza HotPDF correctamente los blend modes de PDF dentro de grupos de transparencia?

Sí, desde la v2.769.3. Un grupo pintado con Multiply u otro blend mode distinto de Normal fusiona su propio color y su propio alfa con el fondo, y su contenido parte de la fusión Normal como exige ISO 32000-1 §11.6.6

¿Por qué HotPDF renderiza dos veces los grupos no aislados?

Un grupo no aislado no tiene plano de alfa propio. HotPDF lo dibuja sobre negro y sobre blanco y recupera el alfa como 1 − (white − black), lo que cuesta algo de velocidad pero deja correctas las sombras Multiply

¿Debería poner RenderColorimetricLuminosityMasks a True?

Déjalo en False para igualar lo que muestran los visores PDF comunes. Ponlo a True solo cuando necesites la lectura CIE Y de ISO 32000-1 para grupos de máscara basados en CIE, por ejemplo en una comparación de conformidad

Los grupos de transparencia con blend mode, las soft masks de luminosidad y el renderizador de páginas que los dibuja forman parte del HotPDF Delphi Component, una biblioteca VCL nativa para Delphi y C++Builder que crea, edita y renderiza documentos PDF sin DLL externas