Technischer Artikel

Blend-Mode-Gruppen und Luminosity-Masks im PDF-Rendering

HotPDF rendert eine PDF-Transparenzgruppe, die mit einem Nicht-Normal-Blend-Mode gemalt wurde, indem es die Gruppe ihren eigenen Blend berechnen lässt: Cb(1 − α) + α·B(Cb, Cs), mit der Gruppen-Alpha, die es verfolgt oder aus einem Schwarz- und einem Weiß-Durchlauf zurückgewinnt. Seit v2.770.0 entscheidet die Property THotPDF.RenderColorimetricLuminosityMasks zudem, ob Luminosity-Soft-Masks Farbe als gewichtetes RGB lesen wie gängige Viewer oder als CIE Y, wie ISO 32000-1 es beschreibt

Beide Änderungen stammen aus derselben Art Datei: einer Seite aus einem Design-Tool-Export, wo ein Produktfoto auf einem weichen Schlagschatten sitzt. Der Schatten ist ein Form XObject, gezeichnet als Transparenzgruppe mit /BM /Multiply, und sein Ausklingen ist eine Luminosity-Soft-Mask. In anderen Viewern ist der Schatten ein helles, neutrales Grau. In älteren HotPDF-Builds kam er fast schwarz heraus und färbte sich Richtung Backdrop-Farbe. Zwei separate Bugs stapelten sich übereinander, und sie lohnen das Auseinandernehmen, denn jeder Renderer, der aus einem „erst opake Objekte“-Design herauswächst, trifft auf beide

Warum rendert ein Multiply-Schlagschatten zu dunkel?

Ein Multiply-Schatten rendert zu dunkel, wenn der Renderer das komponierte Ergebnis der Gruppe blendet statt ihrer eigenen Farbe und Alpha. Die teilweise transparenten Pixel dieses Ergebnisses enthalten den Backdrop bereits, sie mit dem Backdrop zu blenden multipliziert den Backdrop also mit sich selbst. Normales Blenden mit einer Opazität ist linear und überlebt diesen Fehler unbemerkt; Multiply, Screen, Darken und jeder andere nichtlineare Modus tun es nicht

Die HotPDF-Rendering-Pipeline behandelte Blend-Modes ursprünglich pro gemaltem Objekt mit einem simplen, schnellen Modell. Bevor ein Objekt gemalt wird, snapshotet der Renderer die betroffenen Pixel; danach vergleicht er die „davor“- und „danach“-Bitmaps und behandelt die „danach“-Farbe als opake Quelle Cs, berechnet B(Cb, Cs) und komponiert sie mit der aktuellen Opazität. Für einen gefüllten Pfad, eine Glyphe oder ein opakes Bild ist das exakt richtig: Die Quelle ist innerhalb ihrer Form wirklich opak, und die Form ist genau dort, wo sich die Pixel änderten

Eine Transparenzgruppe bricht diese Annahme. ISO 32000-1 §11.4 definiert eine Gruppe als etwas, das zuerst in sein eigenes Ergebnis komponiert wird, mit eigener Farbe und eigener Alpha, und erst danach als ein einzelnes Objekt mit dem Backdrop geblendet wird. Wo ein Schatten zu 30 Prozent opak ist, hält die „danach“-Bitmap etwa 0.3 der Schattenfarbe plus 0.7 der Seite darunter. Das als opake Quelle in Multiply zu füttern macht die Formel zu Backdrop mal (ein Wert, der den Backdrop bereits enthält): Der Schatten dunkelt ab und nimmt den Backdrop-Hue an. Nichts stürzt ab, keine Diagnose feuert, und die Seite sieht schlicht falsch aus – die schlechteste Art, wie ein Renderer failen kann

Wie HotPDF eine Transparenzgruppe selbst blenden lässt

Seit v2.769.3 verlagert HotPDF den Blend einer Gruppe unter einem Nicht-Normal-Blend-Mode aus dem generischen Pro-Objekt-Pfad in den Gruppen-Renderer selbst. Die Gruppe berechnet Cb(1 − α) + α·B(Cb, Cs) direkt aus ihrer eigenen Farbe, ihrer eigenen Alpha und dem Backdrop-Snapshot, der für den Do-Operator genommen wurde, und markiert das Ergebnis dann als bereits geblendet, sodass das umschließende Do es nur noch mit der aktuellen Opazität komponiert, statt ein zweites Mal zu blenden. Einfache Objekte behalten das schnelle Davor-Danach-Modell, und Gruppen unter normalem Blenden bleiben unberührt, weil der lineare Fall nie falsch war

Der harte Teil ist die Alpha. Eine isolierte Gruppe (/I true) startet von einem komplett transparenten Backdrop, der Renderer verfolgt dafür bereits eine separate Alpha-Ebene und kann α direkt ablesen. Eine nicht-isolierte Gruppe komponiert auf das, was schon auf der Seite liegt, und hat keine eigene Alpha-Ebene. HotPDF holt sie zurück, indem es die Gruppe zweimal zeichnet, einmal auf Schwarz und einmal auf Weiß:

  • Auf Schwarz landet jedes Pixel als α·Cs
  • Auf Weiß landet dasselbe Pixel als α·Cs + (1 − α)
  • Subtraktion ergibt α = 1 − (white − black), und dann Cs = black / α

Das funktioniert, weil jedes normale Compositing, das der Renderer ausführt – auch GDI AlphaBlend für Bilder mit Soft-Masks und antialiasten Kanten –, linear im Backdrop ist. Der Trick hat allerdings eine Vorbedingung: Der Gruppeninhalt selbst muss normal blenden. Setzen die eigenen ExtGState-Ressourcen der Gruppe ein nicht-normales /BM, unterscheiden sich die beiden Durchläufe nicht mehr um einen sauberen linearen Term, also teilt HotPDF die Gruppe nicht auf und fällt auf das frühere Verhalten zurück. Die Prüfung deckt nur die eigenen Ressourcen der Gruppe ab, nicht verschachtelte Formen. Der Preis ist auch real: Solche Seiten rendern etwas langsamer, weil die Gruppe zweimal gezeichnet wird. Soft-Masks, die innerhalb der Gruppe gebaut werden, bleiben bis zum Ende beider Durchläufe erhalten, der zweite Durchlauf trifft also den Mask-Cache, statt sie neu zu bauen

// Illustration der Alpha-Wiederherstellung pro Kanal (Werte 0..1).
// Black = Gruppe auf schwarzem Backdrop gezeichnet, White = auf weißem 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;            // komplett transparent: Farbe egal
end;

// Multiply-Blend, dann der Gruppen-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;

Der Code oben ist eine Illustration der Mathematik, keine HotPDF-API; der Renderer tut dasselbe pro Kanal und pro Pixel innerhalb von THotPDF.RenderLoadedPageToBitmap. In der Praxis verliert Alpha, das aus zwei 8-Bit-Durchläufen zurückgewonnen wird, nahe null an Präzision, sehr schwache Kanten tragen also etwas Quantisierungsrauschen – deshalb ist die Division nur dort sinnvoll, wo Alpha klar über null liegt

Der Blend-Mode resettet innerhalb der Gruppe auf Normal

Der Blend-Mode gilt, wie die konstante Alpha und die Soft-Mask, für die Transparenzgruppe als Ganzes und nicht für jedes Objekt in ihr, Gruppeninhalt muss also von normalem Blenden starten (ISO 32000-1 §11.6.6). HotPDF hat früher beim Gruppeneintritt Opazität und Soft-Mask zurückgesetzt, den Blend-Mode aber in Ruhe gelassen. Das Ergebnis innerhalb einer isolierten Gruppe war dramatisch: Der Inhalt multiplizierte sich in den leeren Anfangs-Backdrop der Gruppe, der schwarz ist, und die ganze Gruppe malte schwarz

Dasselbe Reset greift, wenn eine Soft-Mask gebaut wird. Setzt ein ExtGState sowohl /BM /Multiply als auch /SMask, muss die Mask-Gruppe mit normalem Blenden gezeichnet werden; sonst wird eine weiße Mask-Gruppe schwarz und versteckt genau das, was die Mask offenlegen sollte. Beide Resets sind Teil von v2.769.3, und beide sind auf simplen Dokumenten unsichtbar – deshalb haben sie so lange überlebt

Welchen Wert soll eine Luminosity-Soft-Mask für Mittelgrau annehmen?

Nach ISO 32000-1 soll eine Luminosity-Soft-Mask, deren Gruppe einen CIE-basierten Blending-Space benutzt, CIE 1931 Y annehmen, und Y für ein sRGB-Mittelgrau von 0.5 liegt nur bei etwa 0.21, weil Y linear light ist. Gängige PDF-Viewer lösen die Gruppenfarbe stattdessen zu RGB auf und gewichten die kodierten Werte ungefähr 0.30 / 0.59 / 0.11, was für dasselbe Grau etwa 0.5 ergibt. Seit v2.770.0 nutzt HotPDF als Default die viewer-kompatible Gewichtung und bietet THotPDF.RenderColorimetricLuminosityMasks für die Spec-Lesart

Die Lücke ist nicht akademisch. Ein Fade in einem Design-Tool-Export ist meist ein Gradient in einer ICCBased- oder kalibrierten Gruppe, und Mitteltöne sind genau dort, wo lineares Y und gamma-kodiertes Grau am stärksten auseinanderlaufen. Mit CIE Y lässt eine Mittelgrau-Mask nur etwa ein Fünftel des Schattens durch statt der Hälfte, Schlagschatten und Vignetten fallen also spürbar heller aus als in jedem anderen Viewer, in dem der Kunde die Datei öffnet. Nach Buchstaben des Standards recht und nach Augen aller anderen falsch zu sein, ist ein schlechter Deal für einen Default; der Default folgt daher den Viewern, und der strikte Modus ist eine Property entfernt

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('catalog-page.pdf') > 0 then
    begin
      // Default (False): Luminosity-Masks gewichten die aufgelöste Farbe
      // wie DeviceRGB, passend zu dem, was gängige Viewer zeigen.
      // True: CIE Y nehmen, wenn die Mask-Gruppe einen CIE-basierten Space hat.
      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;

Die Property zählt nur, wenn der Blending-Space der Mask-Gruppe CIE-basiert ist: CalGray, CalRGB, Lab oder ICCBased. Masks, deren Gruppe DeviceGray, DeviceRGB oder DeviceCMYK benutzt, rendern unter beiden Einstellungen exakt wie zuvor, denn die Device-Space-Umsetzung nach Grau ist in beiden Modi gleich

Stolperfallen: /BC, /CS und der Render-Cache

Eine Luminosity-Soft-Mask ist nur so konsistent wie ihr Backdrop: Die /BC-Farbe, die den Mask-Bereich außerhalb der Gruppe füllt, muss mit derselben Regel umgesetzt werden wie der Paint innerhalb der Gruppe. HotPDF hat beides in v2.770.0 zusammen geändert. Den Paint mit gewichtetem RGB umzusetzen, den Backdrop aber mit CIE Y, erzeugt eine sichtbare Naht am Ende der Bounding Box der Gruppe – ein Bug, der leicht passiert, wenn der Backdrop durch einen anderen Helper läuft als der Content

Die Gruppenattribute müssen auch gültig sein, bevor all das passiert. HotPDF liest den Blending-Space aus dem /CS-Eintrag der Gruppe und verlangt, dass /BC genau eine Zahl pro Komponente dieses Space trägt: drei für einen RGB-basierten Space, vier für CMYK, eine für Grau. Ein fehlendes /CS oder ein /BC mit falscher Stellenzahl lässt die Mask failen, bevor die Gruppe läuft, und der Paint wird dann unmaskiert gezeichnet statt mit einem erratenen Backdrop maskiert. Separation, DeviceN und NChannel sind keine legalen Blending-Spaces, Spot-Color-Paint innerhalb einer Mask-Gruppe läuft also über seine Tint-Transform in den Alternate Space, bevor die Luminosity berechnet wird

Schließlich ändert die Einstellung Pixel, sie gehört also zum Cache-Key. Mit RenderColorimetricLuminosityMasks an und aus gerenderte Bitmaps werden sowohl im Memory- als auch im Disk-Render-Cache getrennt gecacht, und das Umschalten der Property zwischen Aufrufen von THotPDF.RenderLoadedPageToBitmapCached liefert nie eine Bitmap aus dem anderen Modus. Ein operativer Hinweis: v2.769.3 führte eine Win64-Regression ein, bei der Seiten mit einer Transparenzgruppe ohne eigene Soft-Mask nicht renderten; gefixt in v2.770.1, 64-Bit-Anwendungen sollten diese Release oder neuer benutzen

// Side-by-side-Proofing: dieselbe Seite in beiden Modi rendern.
// Jeder Modus wird unter seinem eigenen Key gecacht, Wiederholungen sind billig.
Pdf.RenderColorimetricLuminosityMasks := False;
ViewerBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);

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

// ... beide zeigen, dann freigeben: gecachte Aufrufe liefern Aufrufer-Kopien
ViewerBmp.Free;
StrictBmp.Free;

Für die breitere Rendering-API – DPI-Wahl, Seiten-Cache und was der Renderer weiterhin annähert – siehe eine PDF-Seite mit HotPDF in ein Delphi-TBitmap rendern. Läuft Ihr Problem in die Gegenrichtung, Transparenz aus einer Datei für Druck oder Archiv entfernen statt sie zu zeichnen, behandelt Soft-Masks und Transparenz-Flattening in PDFlibPas diese Seite

FAQ

Rendert HotPDF PDF-Blend-Modes innerhalb von Transparenzgruppen korrekt?

Ja, seit v2.769.3. Eine Gruppe, die mit Multiply oder einem anderen Nicht-Normal-Blend-Mode gemalt wurde, blendet ihre eigene Farbe und Alpha mit dem Backdrop, und ihr Inhalt startet von normalem Blenden, wie ISO 32000-1 §11.6.6 es verlangt

Warum rendert HotPDF nicht-isolierte Gruppen zweimal?

Eine nicht-isolierte Gruppe hat keine eigene Alpha-Ebene. HotPDF zeichnet sie auf Schwarz und auf Weiß und holt die Alpha als 1 − (white − black) zurück, was etwas Geschwindigkeit kostet, aber Multiply-Schatten korrekt macht

Sollte ich RenderColorimetricLuminosityMasks auf True setzen?

Lassen Sie es False, um zu matchen, was gängige PDF-Viewer zeigen. Setzen Sie es nur auf True, wenn Sie die CIE-Y-Lesart von ISO 32000-1 für CIE-basierte Mask-Gruppen brauchen, etwa in einem Konformitätsvergleich

Blend-Mode-Transparenzgruppen, Luminosity-Soft-Masks und der Seiten-Renderer, der sie zeichnet, sind Teil der HotPDF Delphi Component, einer nativen VCL-Bibliothek für Delphi und C++Builder, die PDF-Dokumente ohne externe DLLs erstellt, bearbeitet und rendert