Artigo Técnico

Grupos de blend mode e máscaras de luminosidade em PDF

O HotPDF renderiza um grupo de transparência PDF pintado com um blend mode não-Normal deixando o grupo calcular a sua própria mistura: Cb(1 − α) + α·B(Cb, Cs), usando o alpha do grupo que segue ou recupera a partir de uma passagem preta e uma branca. Desde a v2.770.0 a propriedade THotPDF.RenderColorimetricLuminosityMasks também escolhe se as soft masks de luminosidade leem a cor como RGB ponderado, como os visualizadores comuns, ou como CIE Y, como a ISO 32000-1 descreve

Ambas as alterações saíram do mesmo tipo de ficheiro: uma página exportada de uma ferramenta de design, onde uma fotografia de produto assenta sobre uma sombra suave. A sombra é um Form XObject desenhado como um grupo de transparência com /BM /Multiply, e o seu esvanecimento é uma soft mask de luminosidade. Noutros visualizadores a sombra é um cinzento claro e neutro. Em builds mais antigas do HotPDF saía quase preta e tingida para a cor do fundo. Dois bugs separados estavam empilhados um sobre o outro, e valem a pena desmontar porque qualquer renderizador que cresça de um design de "objetos opacos primeiro" vai apanhar os dois

Porque é que uma sombra Multiply renderiza demasiado escura?

Uma sombra Multiply renderiza demasiado escura quando o renderizador mistura o resultado composto do grupo em vez da cor e do alpha do próprio grupo. Os pixéis parcialmente transparentes desse resultado já contêm o fundo, por isso misturá-los com o fundo multiplica o fundo por si próprio. A mistura Normal com uma opacidade é linear e sobrevive a este erro sem se notar; Multiply, Screen, Darken e todos os outros modos não lineares não sobrevivem

O pipeline de renderização do HotPDF tratava originalmente os blend modes por objeto pintado com um modelo simples e rápido. Antes de um objeto ser pintado, o renderizador tira um instantâneo dos pixéis afetados; depois de pintar, compara os bitmaps "antes" e "depois" e trata a cor "depois" como uma fonte opaca Cs, calculando B(Cb, Cs) e compondo-a com a opacidade atual. Para um path preenchido, um glifo ou uma imagem opaca isso é exatamente certo: a fonte é realmente opaca dentro da sua forma, e a forma é onde os pixéis mudaram

Um grupo de transparência parte essa suposição. A ISO 32000-1 §11.4 define um grupo como algo que é primeiramente composto no seu próprio resultado, com a sua própria cor e o seu próprio alpha, e só depois misturado com o fundo como um único objeto. Onde uma sombra é 30 por cento opaca, o bitmap "depois" guarda aproximadamente 0.3 da cor da sombra mais 0.7 da página por baixo. Alimente isso ao Multiply como fonte opaca e a fórmula torna-se fundo vezes (um valor que já contém o fundo): a sombra escurece e apanha o tom do fundo. Nada rebenta, nenhum diagnóstico dispara, e a página simplesmente parece errada, que é a pior forma de um renderizador falhar

Como o HotPDF deixa um grupo de transparência fazer o seu próprio blending

Desde a v2.769.3, o HotPDF tira a mistura de um grupo pintado sob um blend mode não-Normal do caminho genérico por objeto e leva-a para o próprio renderizador do grupo. O grupo calcula Cb(1 − α) + α·B(Cb, Cs) diretamente a partir da sua própria cor, do seu próprio alpha e do instantâneo do fundo tirado para o operador Do, e depois marca o resultado como já misturado para o Do exterior apenas o compor com a opacidade atual em vez de misturar uma segunda vez. Objetos simples mantêm o modelo rápido de antes-e-depois, e grupos sob mistura Normal não são afetados porque o caso linear nunca esteve errado

A parte difícil é o alpha. Um grupo isolado (/I true) parte de um fundo totalmente transparente, por isso o renderizador já segue um plano de alpha separado para ele e pode ler o α diretamente. Um grupo não isolado compõe-se sobre o que já estiver na página e não tem plano de alpha próprio. O HotPDF recupera-o desenhando o grupo duas vezes, uma sobre preto e outra sobre branco:

  • Sobre preto, cada pixel acaba como α·Cs
  • Sobre branco, o mesmo pixel acaba como α·Cs + (1 − α)
  • Subtraindo dá α = 1 − (white − black), e depois Cs = black / α

Isto funciona porque todos os compostos Normais que o renderizador executa, incluindo o GDI AlphaBlend para imagens com soft masks e bordas com anti-aliasing, são lineares no fundo. O truque tem uma pré-condição, porém: o conteúdo do grupo em si tem de se misturar normalmente. Se os recursos ExtGState do próprio grupo puserem um /BM não-Normal, as duas passagens deixam de diferir num termo linear limpo, por isso o HotPDF não divide o grupo e recua para o comportamento anterior. A verificação cobre apenas os recursos do próprio grupo, não formulários aninhados. O custo também é real: essas páginas renderizam algo mais lentas porque o grupo é desenhado duas vezes. Soft masks construídas dentro do grupo são guardadas até as duas passagens terminarem, por isso a segunda passagem acerta na cache de máscaras em vez de as reconstruir

// Ilustração da recuperação de alpha por canal (valores 0..1).
// Black = grupo desenhado sobre um backdrop preto, White = sobre um backdrop branco.
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   // un-premultiply
  else
    Source := 0.0;            // totalmente transparente: a cor é irrelevante
end;

// Multiply blend, depois o composto do 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;

O código acima é uma ilustração da matemática, não uma API do HotPDF; o renderizador faz a mesma coisa por canal e por pixel dentro do THotPDF.RenderLoadedPageToBitmap. Na prática, alpha recuperado de duas passagens de 8 bits perde precisão ao aproximar-se de zero, por isso bordas muito ténues trazem algum ruído de quantização, razão pela qual a divisão só faz sentido onde o alpha está claramente acima de zero

O blend mode repõe-se em Normal dentro do grupo

O blend mode, tal como o alpha constante e a soft mask, aplica-se ao grupo de transparência como um todo e não a cada objeto dentro dele, por isso o conteúdo do grupo tem de partir de mistura Normal (ISO 32000-1 §11.6.6). O HotPDF anteriormente repunha a opacidade e a soft mask à entrada do grupo mas deixava o blend mode quieto. O resultado dentro de um grupo isolado era dramático: o conteúdo multiplicava-se pelo fundo inicial vazio do grupo, que é preto, e o grupo inteiro pintava de preto

A mesma reposição aplica-se quando uma soft mask é construída. Se um ExtGState puser simultaneamente /BM /Multiply e /SMask, o grupo da máscara tem de ser desenhado com mistura Normal; caso contrário um grupo de máscara branco torna-se preto e esconde exatamente o que a máscara devia revelar. Ambas as reposições fazem parte da v2.769.3, e ambas são invisíveis em documentos simples, que é como viveram tanto tempo quanto viveram

Que valor deve tomar uma soft mask de luminosidade para cinzento médio?

Sob a ISO 32000-1, uma soft mask de luminosidade cujo grupo usa um espaço de mistura CIE-based deve tomar CIE 1931 Y, e o Y de um cinzento médio sRGB de 0.5 é apenas cerca de 0.21 porque Y é luz linear. Os visualizadores PDF comuns resolvem antes a cor do grupo para RGB e ponderam os valores codificados aproximadamente 0.30 / 0.59 / 0.11, o que dá cerca de 0.5 para o mesmo cinzento. Desde a v2.770.0 o HotPDF usa por predefinição a ponderação compatível com os visualizadores e oferece o THotPDF.RenderColorimetricLuminosityMasks para a leitura da especificação

A diferença não é académica. Um esvanecimento numa exportação de ferramenta de design é normalmente um gradiente num grupo ICCBased ou calibrado, e os meios-tons são exatamente onde o Y linear e o cinzento com codificação gama mais divergem. Com CIE Y, uma máscara cinzento médio deixa passar apenas cerca de um quinto da sombra em vez de metade, por isso sombras projetadas e vinhetas saem visivelmente mais claras do que em qualquer outro visualizador em que o cliente abra o ficheiro. Estar certo pela letra da norma e errado aos olhos de todos é um mau negócio para uma predefinição, por isso a predefinição segue os visualizadores e o modo estrito está a uma propriedade de distância

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('catalog-page.pdf') > 0 then
    begin
      // Predefinição (False): luminosity masks pesam a cor resolvida
      // como DeviceRGB, a par do que os visualizadores comuns mostram.
      // True: toma CIE Y quando o grupo da máscara tem um espaço CIE-based.
      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;

A propriedade só importa quando o espaço de mistura do grupo da máscara é CIE-based: CalGray, CalRGB, Lab ou ICCBased. Máscaras cujo grupo usa DeviceGray, DeviceRGB ou DeviceCMYK renderizam exatamente como antes sob qualquer definição, já que a conversão do espaço de dispositivo para cinzento é a mesma nos dois modos

Armadilhas: /BC, /CS e a cache de renderização

Uma soft mask de luminosidade só é consistente quanto ao seu fundo: a cor /BC que preenche a área da máscara fora do grupo tem de ser convertida com a mesma regra que a pintura dentro do grupo. O HotPDF mudou ambas em conjunto na v2.770.0. Converter a pintura com RGB ponderado mas o fundo com CIE Y produz uma emenda visível onde a bounding box do grupo acaba, um bug fácil de introduzir quando o fundo passa por um helper diferente do conteúdo

Os atributos do grupo também têm de ser válidos antes de tudo isto acontecer. O HotPDF lê o espaço de mistura da entrada /CS do grupo e exige que o /BC traga exatamente um número por componente desse espaço: três para um espaço baseado em RGB, quatro para CMYK, um para cinzento. Um /CS em falta ou um /BC com aridade errada faz a máscara falhar antes de o grupo executar, e a pintura é então desenhada sem máscara em vez de mascarada com um fundo adivinhado. Separation, DeviceN e NChannel não são espaços de mistura legais, por isso pintura de cor direta dentro de um grupo de máscara passa pela sua transformação de tint para o espaço alternativo antes de a luminosidade ser calculada

Por fim, a definição muda pixéis, por isso faz parte da chave de cache. Bitmaps renderizados com o RenderColorimetricLuminosityMasks ligado e desligado são cacheados separadamente tanto na cache de renderização em memória como em disco, e alternar a propriedade entre chamadas ao THotPDF.RenderLoadedPageToBitmapCached nunca devolve um bitmap do outro modo. Uma nota operacional: a v2.769.3 introduziu uma regressão só em Win64 em que páginas com um grupo de transparência que não pusesse soft mask própria falhavam a renderizar; está corrigido na v2.770.1, por isso aplicações de 64 bits devem usar essa versão ou posterior

// Prova lado a lado: renderizar a mesma página nos dois modos.
// Cada modo é cacheado sob a sua própria chave, chamadas repetidas são baratas.
Pdf.RenderColorimetricLuminosityMasks := False;
ViewerBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);

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

// ... mostrar ambos, depois libertá-los: chamadas em cache devolvem cópias do chamador
ViewerBmp.Free;
StrictBmp.Free;

Para a API de renderização mais ampla, incluindo a escolha de DPI, a cache de páginas e o que o renderizador ainda aproxima, veja renderizar uma página PDF para um TBitmap Delphi com o HotPDF. Se o seu problema é a direção oposta, remover transparência de um ficheiro para impressão ou arquivo em vez de a desenhar, soft masks e achatamento de transparência no PDFlibPas cobre esse lado

FAQ

O HotPDF renderiza corretamente blend modes PDF dentro de grupos de transparência?

Sim, desde a v2.769.3. Um grupo pintado com Multiply ou outro blend mode não-Normal mistura a sua própria cor e o seu próprio alpha com o fundo, e o seu conteúdo parte de mistura Normal como a ISO 32000-1 §11.6.6 exige

Porque é que o HotPDF renderiza grupos não isolados duas vezes?

Um grupo não isolado não tem plano de alpha próprio. O HotPDF desenha-o sobre preto e sobre branco e recupera o alpha como 1 − (white − black), o que custa alguma velocidade mas torna as sombras Multiply corretas

Devo pôr RenderColorimetricLuminosityMasks em True?

Deixe em False para igualar o que os visualizadores PDF comuns mostram. Ponha em True apenas quando precisa da leitura CIE Y da ISO 32000-1 para grupos de máscara CIE-based, por exemplo numa comparação de conformidade

Grupos de transparência com blend mode, soft masks de luminosidade e o renderizador de páginas que os desenha fazem parte do HotPDF Delphi Component, uma biblioteca VCL nativa para Delphi e C++Builder que cria, edita e renderiza documentos PDF sem DLLs externas