Artigo Técnico

Blend modes e máscaras de luminosidade na renderização PDF

O HotPDF renderiza um grupo de transparência PDF pintado com um blend mode não-Normal deixando o grupo calcular o seu próprio blend: Cb(1 − α) + α·B(Cb, Cs), usando o alpha do grupo que ele rastreia ou recupera de uma passada em preto e uma em branco. Desde a v2.770.0 a propriedade THotPDF.RenderColorimetricLuminosityMasks também escolhe se soft masks de luminosidade leem cor como RGB ponderado, como os viewers comuns, ou como CIE Y, como a ISO 32000-1 descreve

As duas mudanças saíram do mesmo tipo de arquivo: uma página exportada de uma ferramenta de design, em que a foto de um produto repousa sobre uma sombra suave. A sombra é um Form XObject desenhado como um grupo de transparência com /BM /Multiply, e o seu esmaecimento é uma soft mask de luminosidade. Em outros viewers a sombra é um cinza claro e neutro. Em builds antigos do HotPDF ela saía quase preta e tingida na direção da cor do fundo. Dois bugs separados estavam empilhados um sobre o outro, e vale desmontá-los porque qualquer renderizador que cresça a partir de um design de "objetos opacos primeiro" vai esbarrar nos dois

Por que uma sombra Multiply renderiza escura demais?

Uma sombra Multiply renderiza escura demais quando o renderizador faz o blend do resultado compostado do grupo em vez da própria cor e alpha do grupo. Os pixels parcialmente transparentes desse resultado já contêm o fundo, então fazê-los blend com o fundo multiplica o fundo por ele mesmo. Blend Normal com uma opacidade é linear e sobrevive a esse erro sem ser notado; Multiply, Screen, Darken e todos os outros modos não lineares não

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

Um grupo de transparência quebra essa premissa. A ISO 32000-1 §11.4 define um grupo como algo que primeiro é compostado no seu próprio resultado, com a sua própria cor e o seu próprio alpha, e só então sofre blend 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. Jogue isso no Multiply como fonte opaca e a fórmula vira fundo vezes (um valor que já contém o fundo): a sombra escurece e pega o matiz do fundo. Nada crasha, nenhum diagnóstico dispara, e a página simplesmente fica errada, que é o pior jeito de um renderizador falhar

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

Desde a v2.769.3, o HotPDF tira o blend de um grupo pintado sob um blend mode não-Normal do caminho genérico por objeto e o leva para o próprio renderizador de grupo. O grupo calcula Cb(1 − α) + α·B(Cb, Cs) diretamente a partir da sua própria cor, do seu próprio alpha e do snapshot do fundo tirado para o operador Do, e então marca o resultado como já sofrido blend, para que o Do envolvente apenas o composte com a opacidade atual em vez de sofrer blend uma segunda vez. Objetos simples mantêm o modelo rápido de antes-e-depois, e grupos sob blend 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, então o renderizador já rastreia um plano de alpha separado para ele e pode ler α diretamente. Um grupo não isolado é compostado sobre o que já está na página e não tem plano de alpha próprio. O HotPDF o recupera desenhando o grupo duas vezes, uma em preto e uma em branco:

  • Em preto, cada pixel termina como α·Cs
  • Em branco, o mesmo pixel termina como α·Cs + (1 − α)
  • Subtraindo dá α = 1 − (white − black), e então Cs = black / α

Isso funciona porque todo composite Normal que o renderizador executa, incluindo o AlphaBlend do GDI para imagens com soft masks e bordas com anti-aliasing, é linear no fundo. O truque tem uma pré-condição, porém: o conteúdo do grupo em si precisa sofrer blend normalmente. Se os recursos ExtGState do próprio grupo definem um /BM não-Normal, as duas passadas deixam de diferir por um termo linear limpo, então o HotPDF não divide o grupo e cai de volta no comportamento anterior. A verificação cobre apenas os recursos do próprio grupo, não forms aninhados. O custo também é real: essas páginas renderizam um pouco mais devagar porque o grupo é desenhado duas vezes. Soft masks construídos dentro do grupo são mantidos até que as duas passadas terminem, então a segunda passada acerta o cache de máscaras em vez de reconstruí-los

// Ilustração da recuperação de alpha por canal (valores 0..1).
// Black = grupo desenhado sobre fundo preto, White = sobre fundo 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;

// Blend Multiply, depois o composite 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 passadas de 8 bits perde precisão conforme se aproxima de zero, então bordas muito fracas carregam algum ruído de quantização, que é o motivo de a divisão só fazer sentido onde o alpha está claramente acima de zero

O blend mode reseta para Normal dentro do grupo

O blend mode, como o alpha constante e a soft mask, se aplica ao grupo de transparência como um todo e não a cada objeto dentro dele, então o conteúdo do grupo precisa partir de blend Normal (ISO 32000-1 §11.6.6). O HotPDF antes resetava opacidade e soft mask na entrada do grupo, mas deixava o blend mode em paz. O resultado dentro de um grupo isolado era dramático: o conteúdo se multiplicava no fundo inicial vazio do grupo, que é preto, e o grupo inteiro pintava de preto

O mesmo reset vale quando uma soft mask é construída. Se um ExtGState define tanto /BM /Multiply quanto /SMask, o grupo da máscara precisa ser desenhado com blend Normal; caso contrário, um grupo de máscara branco fica preto e esconde exatamente o que a máscara deveria revelar. Os dois resets fazem parte da v2.769.3, e os dois são invisíveis em documentos simples, que é como viveram tanto tempo

Que valor uma soft mask de luminosidade deve ter para cinza médio?

Sob a ISO 32000-1, uma soft mask de luminosidade cujo grupo usa um espaço de blending baseado em CIE deveria assumir CIE 1931 Y, e Y para um cinza médio sRGB de 0.5 é apenas cerca de 0.21, porque Y é luz linear. Viewers PDF comuns, em vez disso, resolvem a cor do grupo para RGB e ponderam os valores codificados em aproximadamente 0.30 / 0.59 / 0.11, o que dá cerca de 0.5 para o mesmo cinza. Desde a v2.770.0 o HotPDF usa por padrão a ponderação compatível com viewers e oferece o THotPDF.RenderColorimetricLuminosityMasks para a leitura da especificação

A diferença não é acadêmica. Um fade em uma exportação de ferramenta de design costuma ser um gradiente em um grupo ICCBased ou calibrado, e os meios-tons são exatamente onde Y linear e cinza codificado em gamma mais divergem. Com CIE Y, uma máscara de cinza médio deixa passar apenas cerca de um quinto da sombra em vez de metade, então drop shadows e vinhetas saem visivelmente mais leves do que em qualquer outro viewer em que o cliente abrir o arquivo. Estar certo pela letra da norma e errado pelos olhos de todo mundo é uma troca ruim para um padrão, então o padrão segue os viewers e o modo estrito fica 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
      // Padrão (False): máscaras de luminosidade ponderam a cor resolvida
      // como DeviceRGB, casando com o que viewers comuns mostram.
      // True: assuma CIE Y quando o grupo da máscara tiver espaço baseado em 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;

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

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

Uma soft mask de luminosidade é tão consistente quanto o seu fundo: a cor /BC que preenche a área da máscara fora do grupo precisa ser convertida com a mesma regra da pintura dentro do grupo. O HotPDF mudou os dois juntos 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 termina, um bug fácil de introduzir quando o fundo passa por um helper diferente do conteúdo

Os atributos do grupo também precisam ser válidos antes de qualquer isso acontecer. O HotPDF lê o espaço de blending da entrada /CS do grupo e exige que o /BC carregue exatamente um número por componente desse espaço: três para um espaço baseado em RGB, quatro para CMYK, um para cinza. Um /CS ausente 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 ser mascarada com um fundo chutado. Separation, DeviceN e NChannel não são espaços de blending legais, então pintura em spot color dentro de um grupo de máscara passa pelo tint transform para o espaço alternativo antes de a luminosidade ser calculada

Por fim, a configuração muda pixels, então ela faz parte da cache key. Bitmaps renderizados com RenderColorimetricLuminosityMasks ligado e desligado são cacheados separadamente tanto no cache de renderização em memória quanto em disco, e alternar a propriedade entre chamadas ao THotPDF.RenderLoadedPageToBitmapCached nunca retorna um bitmap do outro modo. Uma nota operacional: a v2.769.3 introduziu uma regressão só no Win64 em que páginas com um grupo de transparência que não definia soft mask próprio falhavam em renderizar; corrigido na v2.770.1, então aplicações de 64 bits devem usar essa release ou posterior

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

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

// ... mostre ambos, depois libere: chamadas cacheadas retornam cópias de posse do chamador
ViewerBmp.Free;
StrictBmp.Free;

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

FAQ

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

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

Por 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 o desenha em preto e em branco e recupera o alpha como 1 − (white − black), o que custa um pouco de velocidade mas deixa as sombras Multiply corretas

Devo definir RenderColorimetricLuminosityMasks como True?

Deixe False para casar com o que viewers PDF comuns mostram. Defina como True apenas quando você precisar da leitura CIE Y da ISO 32000-1 para grupos de máscara baseados em CIE, por exemplo em uma 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