Technischer Artikel

Rendern von Separation- und DeviceN-Sonderfarben in Delphi

HotPDF rendert Separation- und DeviceN-Sonderfarben auf geladenen PDF-Seiten, indem der Farbraum über HPDFResolveColorSpace aufgelöst, die Farbton-Transformationsfunktion mit HPDFEvalTintTransform ausgewertet und das Ergebnis über den Ausweichfarbraum (Alternate Space) in RGB für den Bildschirm konvertiert wird. Seit v2.375.0 wertet diese Pipeline alle vier PDF-Funktionstypen aus, einschließlich PostScript-Rechnern des Typs 4, sodass eine druckreife Datei mit Pantone-Farben ihre echten Farben anstelle eines Platzhalters anzeigt. Dieser Artikel beschreibt die Funktionsweise der Pipeline und zeigt gleichermaßen auf, wie sie während der Entwicklung fehlschlug

Der Auslöser ist immer derselbe. Ein Kunde erhält ein PDF von einer Druckerei: Die Überschrift ist in einer benannten Sonderfarbe (Spot-Ink) gesetzt, die Verpackungsgrafik verwendet eine DeviceN-Mischung aus zwei Sonderfarben und der Fließtext ist einfach schwarz. In Acrobat sieht es perfekt aus. In Ihrer Delphi-Anwendung wird die Überschrift schwarz oder im schlimmsten Fall überhaupt nicht gerendert, und der Kunde meldet einen Fehler in Ihrer Software anstelle der Datei. Mit der Datei ist alles in Ordnung. Der Renderer versteht lediglich die von der Datei verwendeten Farbräume nicht

Warum werden Sonderfarben in einem PDF-Viewer schwarz gerendert?

Sonderfarben werden schwarz gerendert oder verschwinden, wenn der Renderer nur die Geräte-Farboperatoren (rg, g, k) implementiert und die generischen Operatoren ignoriert. ISO 32000-1 §8.6 definiert drei Gruppen von Farbräumen: Geräteräume (DeviceGray, DeviceRGB, DeviceCMYK), CIE-basierte Räume (CalGray, CalRGB, Lab, ICCBased) und Spezialräume (Indexed, Separation, DeviceN, Pattern). Alles außerhalb der Gerätegruppe wird mit den generischen Operatoren ausgewählt: cs und CS wählen einen Farbraum namentlich aus dem Ressourcenwörterbuch der Seite aus, und sc, SC, scn sowie SCN liefern die Komponentenwerte. Ein Renderer, der diese Operatoren überspringt, behält die zuletzt eingestellte Farbe bei, was bei einer Seite, die mit einer Überschrift in Sonderfarbe begins, das anfängliche DeviceGray-Schwarz ist

HotPDF hat in v2.333.0 die generischen Operatoren in seinen Seiten-Renderer integriert, zusammen mit einem vereinheitlichten Auflösungspfad: Jeder Ressourcen-Eintrag unter /ColorSpace, sei es ein bloßer Name, ein Inline-Array oder ein indirekter Verweis, wird in einen einzelnen THPDFColorSpace-Datensatz geparst, und jede Füllungs- oder Strichfarbprüfung wird über einen einzigen Aufruf von HPDFResolveColor abgewickelt. Die Auflistung der Farbraumfamilien zeigt die Abdeckung auf einen Blick

type
  THPDFColorSpaceFamily = (csfDeviceGray, csfDeviceRGB, csfDeviceCMYK,
                           csfIndexed, csfCalGray, csfCalRGB, csfLab,
                           csfICCBased, csfSeparation, csfDeviceN,
                           csfUnsupported);

function HPDFResolveColorSpace(Obj: THPDFObject): THPDFColorSpace;

function HPDFResolveColor(const CS: THPDFColorSpace;
  const Comps: THPDFColorComps; CompCount: Integer): THPDFRenderColor;

Eine Design-Entscheidung zahlt sich immer wieder aus: csfUnsupported ist eine reguläre Farbraumfamilie und kein Fehler. Ein Farbraum, den der Renderer nicht interpretieren kann, wird auf einen definierten Ausweichwert heruntergestuft, anstatt das Rendern der Seite abzubrechen. Dies entspricht dem Verhalten gängiger PDF-Viewer und verhindert, dass eine einzige exotische Füllung ein ansonsten renderbares Dokument komplett ausblendet

Wie macht eine Farbton-Transformation aus einem Farbwert eine echte Farbe?

Ein Separation-Farbraum enthält drei Informationen: den Namen der Sonderfarbe, einen alternativen Farbraum und eine Farbton-Transformationsfunktion. Das Array [/Separation /PANTONE485 /DeviceCMYK f] besagt: Wenn der Inhaltsstrom 0.8 scn schreibt, übergebe den Farbtonwert 0,8 an die Funktion f und zeichne das resultierende CMYK-Vierer-Tupel. DeviceN verallgemeinert dies auf N Farben mit einer Funktion mit N Eingängen. Der Farbname selbst dient auf dem Bildschirm nur als Hinweis; die Farbton-Transformation stellt die gesamte Rendering-Semantik dar, sodass ein Renderer, der den Farbraum parst, aber die Funktion überspringt, noch nichts Nützliches getan hat

HPDFEvalTintTransform ist die Funktions-Engine hinter diesem Schritt. Seit v2.334.0 wertet sie exponentielle Funktionen des Typs 2 (C0 + x^N * (C1 - C0) mit Domain- und Range-Clamping), Stitching-Funktionen des Typs 3 (grenzwertselektierte Unterfunktions-Rekursion mit Encode-Remapping) und gesampelte Funktionen des Typs 0 mit 8-, 16- und 32-Bit-Samples aus. Typ 0 entspricht derselben Nachschlagetabellen-Struktur (Lookup-Table), die wir auf der Generatorseite in dem Artikel über den Aufbau von Farb-LUTs des Typs 0 behandelt haben; die Renderseite durchläuft dieselbe Struktur in umgekehrter Reihenfolge, von dekodierten Sample-Bytes zurück zu den Komponentenwerten

PostScript-Rechner-Funktionen des Typs 4 waren die letzte Hürde. Bis v2.375.0 wurden sie elegant auf einen neutralen Platzhalter heruntergestuft; seit v2.375.0 führt HPDFEvalPostScriptCalculator den vollständigen Operatorsatz nach ISO 32000-1 §7.10.5 auf einem begrenzten Operandenstapel aus: Arithmetik, Vergleiche, boolesche und bitweise Operatoren, Stapelmanipulationen einschließlich roll sowie die Bedingungen if/ifelse. Die Randbedingungen sind strenger als sie aussehen. Das PostScript-round rundet halbe Werte in Richtung des größeren Werts auf, sodass Delphis mathematische Standardrundung Round (Banker's Rounding) nicht verwendet werden kann. Die trigonometrischen Operatoren arbeiten in Grad, wobei atan Werte in [0, 360) zurückgibt, und exp is eine Potenz mit zwei Operanden, nicht die natürliche Exponentialfunktion. Derselbe Evaluator treibt auch funktionsbasierte Verlaufsfüllungen an, weshalb das Rendern von axialen und radialen Schattierungen in derselben Version rechnergestützte Rampen erhielt

// Evaluate a Separation/DeviceN tint transform (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
  const Inputs: THPDFColorComps; InputCount: Integer;
  out AltComps: THPDFColorComps): Boolean;

// Type 4 PostScript calculator, ISO 32000-1 7.10.5 operator set.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
  const Inputs: THPDFColorComps; InputCount: Integer;
  var Outputs: THPDFColorComps; OutCount: Integer): Boolean;

Ehrliche Worte zur Präzision: Ein Software-Rechner, der mit doppelter Genauigkeit arbeitet, stimmt nicht Bit für Bit mit einem RIP (Raster Image Processor) überein, und das Clamping an den Bereichsgrenzen (Range) kann um das niederwertigste Bit (LSB) von einer anderen Implementierung abweichen. Für die Bildschirmanzeige und Regressionstests ist das unerheblich; wenn Sie ein farbmanagementgestütztes Proofing entwickeln, ist die Farbton-Transformation nur die erste Stufe und Sie benötigen dahinter ein echtes CMM (Color Management Module)

CalGray, CalRGB, Lab und ICCBased ohne eine ICC-Engine

Die CIE-basierten Familien nehmen den anderen Zweig desselben Resolvers. HotPDF konvertiert Lab-Werte über die standardmäßige Kette Lab zu XYZ zu sRGB, einschließlich des 6/29-Breakpoint-Würfels in der inversen Transferfunktion, und verarbeitet CalRGB mit seinem Gamma-Wert pro Kanal plus einer linearen 3x3-Matrix sowie CalGray mit seinem einzelnen Gamma. Das leistungsrelevante Detail ist die Behandlung des Weißpunkts: Die Umrechnungsmatrix von XYZ in sRGB wird mittels Bradford-Transformation an den im Farbraum deklarierten Weißpunkt angepasst und im aufgelösten THPDFColorSpace-Datensatz zwischengespeichert. So bleibt die Arbeit pro Pixel eine einzige 3x3-Multiplikation, unabhängig davon, wie exotisch die deklarierte Lichtquelle ist

ICCBased-Farbräume erhalten eine bewusst pragmatische Behandlung

ICCBased-Farbräume erhalten eine bewusst pragmatische Behandlung. Die PDF-Spezifikation verlangt, dass jeder ICCBased-Datenstrom einen alternativen Farbraum (/Alternate) deklariert, oder einen implizierten über die Anzahl der Komponenten /N, genau damit Viewer ohne eine Farbmanagement-Engine ihn vernünftig rendern können. HotPDF löst ICCBased über diesen alternativen Farbraum auf, oder schätzt DeviceGray, DeviceRGB oder DeviceCMYK ab, wenn /N 1, 3 oder 4 ist, falls der Eintrag fehlt, und parst die Profil-Bytes niemals. Das bedeutet keine Abhängigkeit von lcms und keine Kosten für die Profilsuche, auf Kosten der farblichen Präzision: Ein ICCBased-Farbraum, dessen Profil stark von seiner Alternative abweicht, zeigt die alternative Darstellung. Für die Bildschirmanzeige und Miniaturansichten ist dies derselbe Kompromiss, den jeder schlanke Viewer eingeht – eine Grenze, die in der eigenen Dokumentation klar genannt werden sollte

Der Fehler, der jede namentliche Farbraumsuche fehlschlagen ließ

Die Operator-Implementierung von v2.333.0 enthielt einen Fehler, der zweiundvierzig Releases lang unbemerkt blieb: Die Handler für cs und CS suchten nach ihrem Operanden mit führendem Schrägstrich (/CS0) im Ressourcenwörterbuch, in dem die Schlüssel ohne Schrägstrich (CS0) gespeichert waren. Jede namentliche Farbraumsuche schlug zu hundert Prozent fehl, und der Code fiel stillschweigend auf den Standard DeviceGray zurück. Das sichtbare Symptom war auf die denkbar subtilste Weise unauffällig: Eine Separation-Füllung von 1 scn wurde zu DeviceGray 1.0, was weiß zeichnet – und weiße Tinte auf einer weißen Seite ist kein Rendering-Fehler, von dem man Screenshots anfertigt. Die Behebung in v2.375.0 bestand in einer gemeinsamen Hilfsfunktion zur Namensnormalisierung, die bei jeder Zuordnung von Operanden zu Ressourcen angewendet wird

Zwei damit verwandte Fehler wurden im Zuge derselben Untersuchung aufgedeckt. Erstens wurden indirekte Verweise auf Objekte mit Array-Werten unaufgelöst zurückgegeben: Der Dokumentenzugriff des Renderers hatte nur typisierte Resolver für Ströme und Wörterbücher. Daher wurde /CS0 5 0 R, das auf ein eigenständiges [/Separation ...]-Array verweist, als roher Verweis zurückgegeben, und der Farbraum wurde als nicht unterstützt eingestuft. Zweitens forderte HPDFReadNumericArray eine strenge Längensemantik, bei der das PDF-Array mindestens so lang sein musste wie der bereitgestellte Puffer. Das Einlesen der Arrays /C0 und /C1 einer Typ-2-Funktion in einen vier-elementigen Puffer schlug daher bei ein- und dreikomponentigen Ausweichräumen fehl, was beide Arrays nullte. So wurde jeder Nicht-CMYK-Exponentialton schwarz gerendert, bis v2.375.0 den toleranten Leser HPDFReadNumericArrayUpTo einführte. Die Lehre daraus: Jedes PDF-Attribut, das bekanntermaßen ein numerisches Array variabler Länge enthält, muss mit einem Reader gelesen werden, der einliest, was existiert – da ein Puffer fester Größe mit strengem Abgleich gültige Dateien in stumme Nullen verwandelt

Wie testet man das Rendern von Sonderfarben, ohne sich selbst zu täuschen?

Die unangenehme Frage ist, warum die Test-Suite trotz alledem fehlerfrei durchlief. Der ursprüngliche Regressionstest SeparationRendersDistinguishable stellte lediglich sicher, dass die gerenderte Bitmap nicht vollständig schwarz war. Eine Pipeline, die jede Sonderfarbe auf DeviceGray reduzierte, erzeugte eine grau-weiße Ausgabe. Da diese nicht schwarz war, bestand der Test, obwohl das gesamte Feature funktionsunfähig war. Schwache Zusicherungen der Form „nicht leer“, „nicht vollständig schwarz“ oder „Prüfsumme ist ungleich Null“ können einen funktionierenden Renderer nicht von einem fehlerhaften unterscheiden, da fast jeder Fehlermodus immer noch einige Pixel erzeugt

Der Zusicherungsstil, der diese Fehler tatsächlich abfängt, sperrt die erwartete Farbe: Rendern Sie ein manuell erstelltes minimales PDF, dessen Separation-Farbe in einen bekannten Farbton aufgelöst wird, und zählen Sie dann die Pixel, in denen dieser Farbton dominiert. Das Rendern in eine Bitmap zur Überprüfung verwendet denselben Einstiegspunkt RenderLoadedPageToBitmap, der in der Anleitung zum Rendern von Seiten in Bitmaps beschrieben ist

var
  Pdf: THotPDF;
  Bmp: TBitmap;
  X, Y, RedHits: Integer;
  Px: TColor;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('spot-red-fixture.pdf', '') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 96);
      try
        RedHits := 0;
        for Y := 0 to Bmp.Height - 1 do
          for X := 0 to Bmp.Width - 1 do
          begin
            Px := Bmp.Canvas.Pixels[X, Y];
            if (GetRValue(Px) > 180) and (GetGValue(Px) < 100) and
               (GetBValue(Px) < 100) then
              Inc(RedHits);
          end;
        // Lock the expected ink: demand a real area of red-dominant
        // pixels, never settle for "not all black".
        Assert(RedHits > 500);
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Das Test-Fixture ist ebenso wichtig wie die Zusicherung. Ein manuell erstelltes PDF von wenigen Hundert Byte Länge, das nur eine einzige Separation-Füllung enthält, lässt keinen Zweifel daran aufkommen, was die erwartete Ausgabe ist. Eine reale Datei durchläuft zwar mehr Code, kann Ihnen jedoch nicht sagen, in welcher Phase der Fehler aufgetreten ist. Wir betrachten das Zählen farbspezifischer Pixel mittlerweile als Mindestanforderung für jeden Rendering-Smoke-Test, da es die einzige Form der Überprüfung ist, die diese Fehler aufdecken konnte

Das Rendern von Sonderfarben und CIE-Farbräumen ist Teil der Pipeline für geladene Dokumente in der HotPDF-Komponente für Delphi und C++Builder, zusammen mit der Funktions-Engine, dem Schattierungs-Rendering und den oben gezeigten Exportpfaden für Bitmaps