技術記事

PDF レンダリングのブレンドモードグループとルミノシティマスク

HotPDF は、Normal 以外のブレンドモードで描かれた PDF トランスペアレンシーグループを、グループ自身にブレンドを計算させることでレンダリングします。Cb(1 − α) + α·B(Cb, Cs) の式を、追跡しているグループアルファ、あるいは黒と白の 2 パスから復元したアルファで評価します。v2.770.0 からは THotPDF.RenderColorimetricLuminosityMasks プロパティが、ルミノシティソフトマスクが色を一般のビューアのように重み付き RGB で読むか、ISO 32000-1 の記述通り CIE Y で読むかも選択します

どちらの変更も、同じ種類のファイルから生まれました。デザインツールから書き出されたページで、製品写真が柔らかいドロップシャドウに載っているものです。シャドウは /BM /Multiply を持つトランスペアレンシーグループとして描かれる Form XObject で、フェードアウトはルミノシティソフトマスクです。他のビューアではシャドウは明るい中立グレーです。旧い HotPDF ビルドではほぼ黒に近く、背景色へ傾いて出ていました。独立した 2 つのバグが積み重なっていたわけです。「不透明オブジェクト優先」設計から育ったレンダラーはどれも両方に当たるため、分解して眺める価値があります

Multiply のドロップシャドウが暗すぎるのはなぜか

Multiply のシャドウが暗すぎるのは、レンダラーがグループ自身の色とアルファではなく、グループの合成結果をブレンドしているときです。その結果の半透明ピクセルは既に背景を含んでいるため、それを背景とブレンドすると背景が背景自身と掛け合わされます。不透明度付きの Normal ブレンドは線形なので、この誤りに気づかれずに耐えます。Multiply、Screen、Darken、その他の非線形モードは耐えません

HotPDF のレンダリングパイプラインは元来、単純で速いモデルで、描画されるオブジェクトごとにブレンドモードを処理していました。オブジェクトを描く前にレンダラーは影響を受けるピクセルをスナップショットし、描いた後に before と after のビットマップを比較します。after の色を不透明なソース Cs と見なし、B(Cb, Cs) を計算して現在の不透明度で合成するのです。塗りパス、グリフ、不透明画像に対しては、これは正確です。ソースはその形状の内側で本当に不透明であり、形状こそピクセルが変わった場所だからです

トランスペアレンシーグループはこの前提を壊します。ISO 32000-1 §11.4 はグループを、独自の色と独自のアルファでまず自身の結果へ合成され、その後に単一のオブジェクトとして背景とブレンドされるものと定義します。シャドウが 30% 不透明な場所では、after のビットマップはおよそ 0.3 のシャドウ色と 0.7 の下のページを含んでいます。これを不透明なソースとして Multiply に流し込むと、式は背景 ×(既に背景を含む値)になります。シャドウは暗くなり、背景の色味を拾います。クラッシュも診断もなく、ページはただ見た目が間違う。レンダラーの失敗の仕方として最悪の形です

HotPDF がトランスペアレンシーグループに自前のブレンドをさせる仕組み

v2.769.3 から、HotPDF は Normal 以外のブレンドモードで描かれるグループのブレンドを、汎用のオブジェクトごとの経路から取り出し、グループレンダラー自身へ移しました。グループは Cb(1 − α) + α·B(Cb, Cs) を、自身の色、自身のアルファ、Do オペレータのために撮られた背景スナップショットから直接計算し、結果に「既にブレンド済み」の印を付けます。外側の Do は 2 度目のブレンドの代わりに、現在の不透明度での合成だけを行います。普通のオブジェクトは速い before/after モデルを保ち、Normal ブレンド下のグループは影響を受けません。線形のケースは最初から間違っていなかったからです

厄介なのはアルファです。分離グループ(/I true)は完全に透明な背景から始まるため、レンダラーは既に独立したアルファプレーンを追跡していて、α を直接読めます。非分離グループはページに既にあるものの上へ合成され、自分のアルファプレーンを持ちません。HotPDF はグループを 2 回、黒の上に 1 回、白の上に 1 回描くことでこれを復元します

  • 黒の上では、各ピクセルは α·Cs で終わります
  • 白の上では、同じピクセルは α·Cs + (1 − α) で終わります
  • 差を取ると α = 1 − (white − black)、続いて Cs = black / α が得られます

これが機能するのは、レンダラーが行うすべての Normal 合成——ソフトマスクとアンチエイリアス付きエッジの画像に対する GDI の AlphaBlend を含む——が背景に関して線形だからです。ただしこのトリックには前提があります。グループコンテンツ自体が Normal にブレンドされなければなりません。グループ自身の ExtGState リソースが Normal 以外の /BM を設定している場合、2 つのパスはもはやきれいな線形項では違いません。そのため HotPDF はグループを分割せず、従来の動作にフォールバックします。この確認はグループ自身のリソースだけを対象とし、ネストしたフォームは対象外です。コストも実在します。グループが 2 回描かれるため、そのようなページはいくらか遅くレンダリングされます。グループ内部で構築されたソフトマスクは両方のパスが終わるまで保持されるため、2 パス目は再構築の代わりにマスクキャッシュに当たります

// チャネルごとのアルファ復元の概念例(値は 0..1)。
// Black = 黒背景に描いたグループ、White = 白背景に描いたグループ。
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   // アルファを割って戻す
  else
    Source := 0.0;            // 完全透明: 色は無関係
end;

// Multiply ブレンド、その後グループ合成 Cb(1 - a) + a * B(Cb, Cs)
function CompositeMultiply(Backdrop, Source, Alpha: Double): Double;
begin
  Result := Backdrop * (1.0 - Alpha) + Alpha * (Backdrop * Source);
end;

上のコードは数学の概念例であって HotPDF の API ではありません。レンダラーは同じことを THotPDF.RenderLoadedPageToBitmap の内部で、チャネルごと、ピクセルごとに行います。実務では、2 つの 8 ビットパスから復元したアルファはゼロに近づくにつれ精度を失うため、ごく薄いエッジには量子化ノイズが乗ります。割り算が意味を持つのはアルファが明確にゼロより上の場所だけなのは、このためです

グループ内部でブレンドモードは Normal に戻る

ブレンドモードは、定数アルファやソフトマスクと同じく、トランスペアレンシーグループ全体に適用されるものであって、内部の各オブジェクトにではありません。そのためグループコンテンツは Normal ブレンドから始まらねばなりません(ISO 32000-1 §11.6.6)。以前の HotPDF はグループに入るとき不透明度とソフトマスクをリセットしていましたが、ブレンドモードはそのままにしていました。分離グループの内部での結果は派手でした。コンテンツは、黒であるグループの空の初期背景へ自分自身を掛け合わせ、グループ全体が黒く塗れたのです

ソフトマスクを構築するときにも同じリセットが適用されます。1 つの ExtGState が /BM /Multiply と /SMask の両方を設定している場合、マスクグループは Normal ブレンドで描かれねばなりません。そうしないと白いマスクグループが黒く化け、マスクが明かすはずだったものをちょうど隠してしまいます。どちらのリセットも v2.769.3 の一部であり、どちらも単純な文書では見えません。だからこそ、あんなに長く生き延びていたのです

中間グレーに対してルミノシティソフトマスクはどんな値を取るべきか

ISO 32000-1 では、グループが CIE ベースのブレンディングスペースを使うルミノシティソフトマスクは CIE 1931 の Y を取るべきです。そして sRGB の中間グレー 0.5 に対する Y はわずか約 0.21 です。Y は線形光だからです。一般の PDF ビューアはその代わり、グループの色を RGB へ解決し、符号化された値に大体 0.30 / 0.59 / 0.11 の重みを付けます。同じグレーで約 0.5 になります。v2.770.0 から HotPDF はビューア互換の重み付けを既定とし、規格通りの読み方のために THotPDF.RenderColorimetricLuminosityMasks を提供します

この差は学問の話ではありません。デザインツールの書き出しにおけるフェードはたいてい ICCBased か calibrated グループの中のグラデーションであり、中間調は線形 Y とガンマ符号化グレーが最も乖離する場所です。CIE Y では、中間グレーのマスクは半分ではなく影の約 5 分の 1 しか通さないため、ドロップシャドウとビネットは、顧客がファイルを開く他のどのビューアよりも目立って薄く出ます。規格の文字通りには正しく、誰の目にも間違っているというのは、既定値としては悪い取引です。だから既定はビューアに従い、厳格モードはプロパティ 1 つの距離に置いてあります

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('catalog-page.pdf') > 0 then
    begin
      // 既定 (False): ルミノシティマスクは解決後の色を
      // DeviceRGB のように重み付けし、一般のビューア表示に合わせる
      // True: マスクグループが CIE ベース空間なら CIE Y を取る
      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;

このプロパティが効くのは、マスクグループのブレンディングスペースが CIE ベース——CalGray、CalRGB、Lab、ICCBased——のときだけです。グループが DeviceGray、DeviceRGB、DeviceCMYK を使うマスクは、どちらの設定でも以前とまったく同じようにレンダリングされます。デバイススペースからグレーへの変換は両モードで同じだからです

落とし穴: /BC、/CS、レンダーキャッシュ

ルミノシティソフトマスクの一貫性は背景と同じだけです。グループの外のマスク領域を埋める /BC の色は、グループ内部のペイントと同じ規則で変換されねばなりません。HotPDF は v2.770.0 で両方を一緒に変えました。ペイントを重み付き RGB で、背景を CIE Y で変換すると、グループのバウンディングボックスが終わる場所に目に見える継ぎ目ができます。背景がコンテンツと別のヘルパーを通るときに生じやすいバグです

グループ属性も、このいずれかが始まる前に有効でなければなりません。HotPDF はブレンディングスペースをグループの /CS エントリから読み、/BC がその空間のコンポーネントごとにちょうど 1 個の数値を運ぶことを要求します。RGB ベースの空間なら 3、CMYK なら 4、グレーなら 1 です。/CS の欠落や個数の合わない /BC があると、マスクはグループの実行前に失敗し、ペイントは推測の背景でマスクされる代わりに、マスクなしで描かれます。Separation、DeviceN、NChannel は合法なブレンディングスペースではないため、マスクグループ内のスポットカラーペイントは、ルミノシティが計算される前に、tint transform を通じて代替スペースへ行きます

最後に、この設定はピクセルを変えるため、キャッシュキーの一部です。RenderColorimetricLuminosityMasks をオンとオフでレンダリングされたビットマップは、メモリとディスクの両レンダーキャッシュで別々にキャッシュされ、THotPDF.RenderLoadedPageToBitmapCached の呼び出しの間でプロパティを切り替えても、別モードのビットマップが返ることはありません。運用上の注意が 1 つ。v2.769.3 は Win64 専用のリグレッションを持ち込みました。自分のソフトマスクを設定しないトランスペアレンシーグループを含むページがレンダリングに失敗するものです。v2.770.1 で修正済みのため、64 ビットアプリケーションはそのリリース以降を使ってください

// 並べて校正: 同じページを両モードでレンダリングする。
// 各モードは固有のキーでキャッシュされるため、繰り返し呼び出しは安い。
Pdf.RenderColorimetricLuminosityMasks := False;
ViewerBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);

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

// ... 両方を表示し、その後解放する。キャッシュ呼び出しは呼び出し側所有のコピーを返す
ViewerBmp.Free;
StrictBmp.Free;

DPI の選択、ページキャッシュ、レンダラーがまだ近似しているものを含む、より広範なレンダリング API についてはHotPDF で PDF ページを Delphi の TBitmap にレンダリングするを参照してください。問題が逆方向、つまり透明性を描くのではなく印刷やアーカイブのためにファイルから取り除くことなら、PDFlibPas のソフトマスクとトランスペアレンシーフラットニングがその側を扱います

FAQ

HotPDF はトランスペアレンシーグループ内の PDF ブレンドモードを正しくレンダリングするのか

はい。v2.769.3 からです。Multiply やその他の Normal 以外のブレンドモードで描かれたグループは、自身の色とアルファを背景とブレンドし、そのコンテンツは ISO 32000-1 §11.6.6 が要求する通り Normal ブレンドから始まります

HotPDF は非分離グループを 2 回レンダリングするのはなぜか

非分離グループには自分のアルファプレーンがありません。HotPDF は黒と白の上に描き、アルファを 1 − (white − black) として復元します。速度をいくらか犠牲にする代わりに、Multiply のシャドウが正しくなります

RenderColorimetricLuminosityMasks を True にすべきか

一般の PDF ビューアの表示に合わせるなら False のままにします。True にするのは、CIE ベースのマスクグループに対する ISO 32000-1 の CIE Y 読みが必要なとき、たとえば適合性比較の場面だけです

ブレンドモードのトランスペアレンシーグループ、ルミノシティソフトマスク、そしてそれらを描くページレンダラーは、HotPDF Delphi Component の一部です。外部 DLL なしで PDF 文書を作成、編集、レンダリングする、Delphi と C++Builder 向けのネイティブ VCL ライブラリです