Technical Article

Blend-Mode Groups and Luminosity Masks in PDF Rendering

HotPDF renders a PDF transparency group painted with a non-Normal blend mode by letting the group compute its own blend: Cb(1 − α) + α·B(Cb, Cs), using the group alpha it tracks or recovers from a black and a white pass. Since v2.770.0 the THotPDF.RenderColorimetricLuminosityMasks property also chooses whether luminosity soft masks read colour as weighted RGB, like common viewers, or as CIE Y, as ISO 32000-1 describes

Both changes came out of the same kind of file: a page exported from a design tool, where a product photo sits on a soft drop shadow. The shadow is a Form XObject drawn as a transparency group with /BM /Multiply, and its fade-out is a luminosity soft mask. In other viewers the shadow is a light, neutral grey. In older HotPDF builds it came out almost black and tinted towards the backdrop colour. Two separate bugs were stacked on top of each other, and they are worth taking apart because any renderer that grows out of an "opaque objects first" design will hit both

Why does a Multiply drop shadow render too dark?

A Multiply shadow renders too dark when the renderer blends the group's composited result instead of the group's own colour and alpha. The partly transparent pixels of that result already contain the backdrop, so blending them with the backdrop multiplies the backdrop by itself. Normal blending with an opacity is linear and survives this mistake unnoticed; Multiply, Screen, Darken and every other non-linear mode do not

The HotPDF rendering pipeline originally handled blend modes per painted object with a simple and fast model. Before an object is painted, the renderer snapshots the affected pixels; after painting, it compares the "before" and "after" bitmaps and treats the "after" colour as an opaque source Cs, computing B(Cb, Cs) and compositing it with the current opacity. For a filled path, a glyph or an opaque image that is exactly right: the source really is opaque inside its shape, and the shape is where the pixels changed

A transparency group breaks that assumption. ISO 32000-1 §11.4 defines a group as something that is composited first into its own result, with its own colour and its own alpha, and only then blended with the backdrop as a single object. Where a shadow is 30 percent opaque, the "after" bitmap holds roughly 0.3 of the shadow colour plus 0.7 of the page beneath it. Feed that into Multiply as an opaque source and the formula becomes backdrop times (a value that already contains the backdrop): the shadow darkens and picks up the backdrop's hue. Nothing crashes, no diagnostic fires, and the page simply looks wrong, which is the worst way for a renderer to fail

How HotPDF lets a transparency group do its own blending

Since v2.769.3, HotPDF moves the blend for a group painted under a non-Normal blend mode out of the generic per-object path and into the group renderer itself. The group computes Cb(1 − α) + α·B(Cb, Cs) directly from its own colour, its own alpha and the backdrop snapshot taken for the Do operator, then marks the result as already blended so the enclosing Do only composites it with the current opacity instead of blending a second time. Plain objects keep the fast before-and-after model, and groups under Normal blending are unaffected because the linear case was never wrong

The hard part is alpha. An isolated group (/I true) starts from a fully transparent backdrop, so the renderer already tracks a separate alpha plane for it and can read α directly. A non-isolated group composites onto whatever is already on the page and has no alpha plane of its own. HotPDF recovers it by drawing the group twice, once on black and once on white:

  • On black, each pixel ends up as α·Cs
  • On white, the same pixel ends up as α·Cs + (1 − α)
  • Subtracting gives α = 1 − (white − black), and then Cs = black / α

This works because every Normal composite the renderer performs, including GDI AlphaBlend for images with soft masks and anti-aliased edges, is linear in the backdrop. The trick has a precondition, though: the group content itself must blend normally. If the group's own ExtGState resources set a non-Normal /BM, the two passes no longer differ by a clean linear term, so HotPDF does not split the group and falls back to the previous behavior. The check covers only the group's own resources, not nested forms. The cost is also real: such pages render somewhat slower because the group is drawn twice. Soft masks built inside the group are kept until both passes finish, so the second pass hits the mask cache instead of rebuilding them

// Illustration of the alpha recovery per channel (values 0..1).
// Black = group drawn on a black backdrop, White = on a white backdrop.
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;            // fully transparent: colour is irrelevant
end;

// Multiply blend, then the group composite Cb(1 - a) + a * B(Cb, Cs)
function CompositeMultiply(Backdrop, Source, Alpha: Double): Double;
begin
  Result := Backdrop * (1.0 - Alpha) + Alpha * (Backdrop * Source);
end;

The code above is an illustration of the math, not a HotPDF API; the renderer does the same thing per channel and per pixel inside THotPDF.RenderLoadedPageToBitmap. In practice, alpha recovered from two 8-bit passes loses precision as it approaches zero, so very faint edges carry some quantization noise, which is why the division is only meaningful where alpha is clearly above zero

The blend mode resets to Normal inside the group

The blend mode, like the constant alpha and the soft mask, applies to the transparency group as a whole and not to each object inside it, so group content must start from Normal blending (ISO 32000-1 §11.6.6). HotPDF previously reset opacity and soft mask on group entry but left the blend mode alone. The result inside an isolated group was dramatic: the content multiplied itself into the group's empty initial backdrop, which is black, and the whole group painted black

The same reset applies when a soft mask is built. If one ExtGState sets both /BM /Multiply and /SMask, the mask group must be drawn with Normal blending; otherwise a white mask group turns black and hides exactly what the mask was supposed to reveal. Both resets are part of v2.769.3, and both are invisible on simple documents, which is how they lived as long as they did

What value should a luminosity soft mask take for mid-grey?

Under ISO 32000-1 a luminosity soft mask whose group uses a CIE-based blending space should take CIE 1931 Y, and Y for an sRGB mid-grey of 0.5 is only about 0.21 because Y is linear light. Common PDF viewers instead resolve the group colour to RGB and weight the encoded values roughly 0.30 / 0.59 / 0.11, which gives about 0.5 for the same grey. Since v2.770.0 HotPDF defaults to the viewer-compatible weighting and offers THotPDF.RenderColorimetricLuminosityMasks for the specification reading

The gap is not academic. A fade in a design-tool export is usually a gradient in an ICCBased or calibrated group, and mid-tones are exactly where linear Y and gamma-encoded grey diverge most. With CIE Y, a mid-grey mask lets only about a fifth of the shadow through instead of half, so drop shadows and vignettes come out noticeably lighter than in every other viewer the customer opens the file in. Being right by the letter of the standard and wrong by everyone's eyes is a bad trade for a default, so the default follows the viewers and the strict mode is one property away

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('catalog-page.pdf') > 0 then
    begin
      // Default (False): luminosity masks weight the resolved colour
      // like DeviceRGB, matching what common viewers show.
      // True: take CIE Y when the mask group has a CIE-based space.
      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;

The property only matters when the mask group's blending space is CIE-based: CalGray, CalRGB, Lab or ICCBased. Masks whose group uses DeviceGray, DeviceRGB or DeviceCMYK render exactly as before under either setting, since the device-space conversion to grey is the same in both modes

Pitfalls: /BC, /CS and the render cache

A luminosity soft mask is only as consistent as its backdrop: the /BC colour that fills the mask area outside the group must be converted with the same rule as the paint inside the group. HotPDF changed both together in v2.770.0. Converting the paint with weighted RGB but the backdrop with CIE Y produces a visible seam where the group's bounding box ends, a bug that is easy to introduce when the backdrop goes through a different helper than the content

The group attributes also have to be valid before any of this happens. HotPDF reads the blending space from the group's /CS entry and requires /BC to carry exactly one number per component of that space: three for an RGB-based space, four for CMYK, one for grey. A missing /CS or a wrong-arity /BC makes the mask fail before the group executes, and the paint is then drawn unmasked rather than masked with a guessed backdrop. Separation, DeviceN and NChannel are not legal blending spaces, so spot-colour paint inside a mask group goes through its tint transform to the alternate space before luminosity is computed

Finally, the setting changes pixels, so it is part of the cache key. Bitmaps rendered with RenderColorimetricLuminosityMasks on and off are cached separately in both the memory and disk render caches, and toggling the property between calls to THotPDF.RenderLoadedPageToBitmapCached never returns a bitmap from the other mode. One operational note: v2.769.3 introduced a Win64-only regression in which pages with a transparency group that set no soft mask of their own failed to render; it is fixed in v2.770.1, so 64-bit applications should use that release or later

// Side-by-side proofing: render the same page in both modes.
// Each mode is cached under its own key, so repeated calls are cheap.
Pdf.RenderColorimetricLuminosityMasks := False;
ViewerBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);

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

// ... show both, then free them: cached calls return caller-owned copies
ViewerBmp.Free;
StrictBmp.Free;

For the broader rendering API, including DPI choice, the page cache and what the renderer still approximates, see rendering a PDF page to a Delphi TBitmap with HotPDF. If your problem is the opposite direction, removing transparency from a file for print or archival rather than drawing it, soft masks and transparency flattening in PDFlibPas covers that side

FAQ

Does HotPDF render PDF blend modes inside transparency groups correctly?

Yes, since v2.769.3. A group painted with Multiply or another non-Normal blend mode blends its own colour and alpha with the backdrop, and its content starts from Normal blending as ISO 32000-1 §11.6.6 requires

Why does HotPDF render non-isolated groups twice?

A non-isolated group has no alpha plane of its own. HotPDF draws it on black and on white and recovers alpha as 1 − (white − black), which costs some speed but makes Multiply shadows correct

Should I set RenderColorimetricLuminosityMasks to True?

Leave it False to match what common PDF viewers show. Set it to True only when you need the CIE Y reading of ISO 32000-1 for CIE-based mask groups, for example in a conformance comparison

Blend-mode transparency groups, luminosity soft masks and the page renderer that draws them are part of the HotPDF Delphi Component, a native VCL library for Delphi and C++Builder that creates, edits and renders PDF documents without external DLLs