HotPDF rendert Separation- und DeviceN-Schmuckfarben auf geladenen PDF-Seiten, indem es den Farbraum über HPDFResolveColorSpace auflöst, die Tint-Transform-Funktion mit HPDFEvalTintTransform auswertet und das Ergebnis über den Alternativfarbraum für den Bildschirm nach RGB konvertiert. Seit v2.375.0 wertet diese Pipeline alle vier PDF-Funktionstypen aus, einschließlich Type-4-PostScript-Calculatoren, sodass eine druckfertige Datei mit Pantone-Farben ihre echten Farben statt eines Platzhalters anzeigt. Dieser Artikel erklärt, wie die Pipeline funktioniert, und, ebenso nützlich, wie sie während der Entwicklung versagt hat
Der Auslöser ist immer derselbe. Ein Kunde erhält ein PDF von einer Druckerei: Die Überschrift ist in einer benannten Schmuckfarbe gesetzt, das Verpackungs-Artwork verwendet eine DeviceN-Mischung aus zwei Druckfarben, und der Fließtext ist reines Schwarz. In Acrobat sieht es perfekt aus. In Ihrer Delphi-Anwendung wird die Überschrift schwarz gerendert, oder schlimmer, gar nicht, und der Kunde meldet einen Fehler gegen Ihre Software statt gegen die Datei. Die Datei ist in Ordnung. Der Renderer spricht nur die Farbräume nicht, die die Datei verwendet
Warum werden Schmuckfarben in einem PDF-Viewer schwarz gerendert?
Schmuckfarben werden schwarz gerendert oder verschwinden, wenn der Renderer nur die Geräte-Farboperatoren (rg, g, k) implementiert und die generischen 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 Raum per Name aus dem Ressourcen-Dictionary der Seite, dann liefern sc, SC, scn und SCN die Komponentenwerte. Ein Renderer, der diese Operatoren überspringt, behält die zuletzt gesetzte Farbe, was für eine Seite, die mit ihrer Schmuckfarben-Überschrift beginnt, das anfängliche DeviceGray-Schwarz ist
HotPDF hat den generischen Operatorsatz in v2.333.0 zu seinem Seiten-Renderer hinzugefügt, zusammen mit einem einheitlichen Auflösungspfad: Jeder /ColorSpace-Ressourceneintrag, ob bloßer Name, Inline-Array oder indirekte Referenz, wird in einen einzigen THPDFColorSpace-Record geparst, und jede Füll- oder Konturfarbanforderung läuft durch einen einzigen HPDFResolveColor-Aufruf. Die Familien-Enumeration 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 Designentscheidung zahlt sich immer wieder aus: csfUnsupported ist eine vollwertige Familie, kein Fehler. Ein Raum, den der Renderer nicht interpretieren kann, fällt auf einen definierten Fallback zurück, statt die Seite abzubrechen, was dem Verhalten gängiger Viewer entspricht und verhindert, dass eine einzelne exotische Füllung ein ansonsten darstellbares Dokument leert
Wie verwandelt eine Tint-Transform einen Druckfarbenwert in eine echte Farbe?
Ein Separation-Farbraum trägt drei Informationen: den Namen der Druckfarbe, einen Alternativfarbraum und eine Tint-Transform-Funktion. Das Array [/Separation /PANTONE485 /DeviceCMYK f] sagt: Wenn der Content-Stream 0.8 scn schreibt, führe den Tint-Wert 0,8 in die Funktion f ein und male das resultierende CMYK-Quadrupel. DeviceN verallgemeinert dies auf N Druckfarben mit einer Funktion mit N Eingängen. Der Name der Druckfarbe selbst ist am Bildschirm nur ein Hinweis; die Tint-Transform ist die gesamte Rendering-Semantik, sodass ein Renderer, der den Raum parst, aber die Funktion überspringt, noch nichts Nützliches getan hat
HPDFEvalTintTransform ist die Funktions-Engine hinter diesem Schritt. Eingeführt in v2.334.0, wertet sie Type-2-Exponentialfunktionen (C0 + x^N * (C1 - C0) mit Domain- und Range-Begrenzung), Type-3-Stitching-Funktionen (über Bounds ausgewählte Unterfunktions-Rekursion mit Encode-Umschlüsselung) und Type-0-Sampled-Funktionen mit 8-, 16- und 32-Bit-Samples aus. Type 0 ist dieselbe Lookup-Tabellen-Mechanik, die wir von der Erzeugungsseite in dem Artikel über den Aufbau von Type-0-Farb-LUTs behandelt haben; die Render-Seite durchläuft die identische Struktur rückwärts, von decodierten Sample-Bytes zurück zu Komponentenwerten
Type-4-PostScript-Calculator-Funktionen waren die letzte Bastion. Bis v2.375.0 fielen sie sanft auf einen neutralen Platzhalter zurück; seit v2.375.0 führt HPDFEvalPostScriptCalculator den vollständigen Operatorsatz aus ISO 32000-1 §7.10.5 auf einem begrenzten Operandenstapel aus: arithmetische, Vergleichs-, boolesche und bitweise Operatoren, Stapelmanipulation einschließlich roll sowie if/ifelse-Bedingungen. Die Randsemantik ist strenger, als sie aussieht. PostScript-round rundet bei der Hälfte zum größeren Wert, sodass Delphis kaufmännisch rundendes Round nicht verwendet werden kann; die trigonometrischen Operatoren arbeiten in Grad, wobei atan Werte in [0, 360) zurückgibt; und exp ist eine Potenz mit zwei Operanden, nicht die natürliche Exponentialfunktion. Derselbe Auswerter treibt auch funktionsbasierte Verlaufsfüllungen an, weshalb das Rendering axialer und radialer Shadings im selben Release calculatorgesteuerte Rampen erhielt
// Eine Separation-/DeviceN-Tint-Transform auswerten (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
const Inputs: THPDFColorComps; InputCount: Integer;
out AltComps: THPDFColorComps): Boolean;
// Type-4-PostScript-Calculator, Operatorsatz aus ISO 32000-1 7.10.5.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
const Inputs: THPDFColorComps; InputCount: Integer;
var Outputs: THPDFColorComps; OutCount: Integer): Boolean;
Ehrlichkeit bei der Präzision: Ein in doppelter Genauigkeit ausgewerteter Software-Calculator wird einem RIP nicht Bit für Bit entsprechen, und das Begrenzen an der Range-Grenze kann sich um ein niedrigstwertiges Bit von einer anderen Implementierung unterscheiden. Für Bildschirmanzeige und Regressions-Rendering ist das irrelevant; wenn Sie farbverbindliches Proofing bauen, ist die Tint-Transform nur die erste Stufe, und Sie brauchen nachgelagert weiterhin ein echtes CMM
CalGray, CalRGB, Lab und ICCBased ohne ICC-Engine
Die CIE-basierten Familien nehmen den anderen Zweig desselben Resolvers. HotPDF konvertiert Lab-Werte über die Standardkette Lab nach XYZ nach sRGB, einschließlich des 6/29-Knickpunkt-Kubus in der inversen Übertragungsfunktion, und behandelt CalRGB mit seinem Gamma pro Kanal plus linearer 3x3-Matrix und CalGray mit seinem einzelnen Gamma. Das leistungsrelevante Detail ist die Weißpunktbehandlung: Die XYZ-nach-sRGB-Konvertierungsmatrix wird per Bradford an den im Farbraum deklarierten Weißpunkt adaptiert und im aufgelösten THPDFColorSpace-Record gecacht, sodass die Arbeit pro Pixel eine einzige 3x3-Multiplikation bleibt, egal wie exotisch die deklarierte Lichtart ist
ICCBased-Räume erhalten eine bewusst pragmatische Behandlung. Die PDF-Spezifikation verlangt, dass jeder ICCBased-Stream einen /Alternate-Raum deklariert oder einen impliziten über seine Komponentenanzahl /N, gerade damit Viewer ohne Farbmanagement-Engine trotzdem sinnvoll rendern können. HotPDF löst ICCBased über diesen Alternativraum auf oder rät DeviceGray, DeviceRGB oder DeviceCMYK daraus, dass /N 1, 3 oder 4 ist, wenn der Eintrag fehlt, und parst die Profilbytes nie. Das bedeutet keine lcms-Abhängigkeit und keine Kosten für Profil-Lookups, um den Preis kolorimetrischer Genauigkeit: Ein ICCBased-Raum, dessen Profil stark von seinem Alternativraum abweicht, zeigt die Darstellung des Alternativraums. Für die Bildschirmanzeige und Miniaturansichten ist das derselbe Kompromiss, den jeder leichtgewichtige Viewer eingeht, und es ist die Grenze, die Sie in Ihrer eigenen Dokumentation klar benennen sollten
Der Fehler, durch den jeder benannte Farbraum-Lookup danebenging
Die Operator-Verdrahtung aus v2.333.0 wurde mit einem Defekt ausgeliefert, der zweiundvierzig Releases lang unsichtbar blieb: Die cs- und CS-Handler suchten ihren Operanden mit intaktem führendem Schrägstrich (/CS0) in Ressourcen-Dictionary-Schlüsseln, die ohne Schrägstrich gespeichert waren (CS0). Jeder benannte Farbraum-Lookup ging daneben, in hundert Prozent der Fälle, und der Code fiel still auf das Standard-DeviceGray zurück. Das sichtbare Symptom war auf die schlimmste Weise subtil: Eine Separation-Füllung mit 1 scn wurde zu DeviceGray 1.0, was Weiß malt, und weiße Druckfarbe auf einer weißen Seite ist kein Rendering-Fehler, von dem jemand einen Screenshot macht. Die Korrektur in v2.375.0 war ein gemeinsamer Namensnormalisierungs-Helfer, der bei jedem Operand-zu-Ressource-Lookup angewendet wird
Zwei Geschwisterdefekte fielen bei derselben Untersuchung ab. Erstens wurden indirekte Referenzen auf Array-Objekte unaufgelöst zurückgegeben: Der Dokumentzugriff des Renderers hatte typisierte Resolver nur für Streams und Dictionaries, sodass /CS0 5 0 R, das auf ein eigenständiges [/Separation ...]-Array zeigte, als rohe Referenz zurückkam und der Raum als nicht unterstützt geparst wurde. Zweitens erzwang HPDFReadNumericArray strikte Längensemantik und verlangte, dass das PDF-Array mindestens so lang ist wie der übergebene Puffer. Das Lesen von /C0 und /C1 einer Type-2-Funktion in einen Vier-Elemente-Puffer schlug daher bei Alternativräumen mit einer und drei Komponenten fehl, ließ beide Arrays auf null und jede Nicht-CMYK-Exponential-Tint schwarz rendern, bis v2.376.0 den toleranten Reader HPDFReadNumericArrayUpTo einführte. Die übertragbare Lehre: Jeder PDF-Schlüssel, der laut Dokumentation ein numerisches Array variabler Länge enthält, muss mit einem Reader gelesen werden, der füllt, was vorhanden ist, denn ein Puffer fester Größe mit strikter Übereinstimmung verwandelt gültige Dateien in stille Nullen
Wie testet man Schmuckfarben-Rendering, ohne sich selbst zu täuschen?
Die unangenehme Frage lautet, warum die Testsuite während all dessen grün blieb. Die ursprüngliche Regression, SeparationRendersDistinguishable, prüfte nur, dass die gerenderte Bitmap nicht vollständig schwarz war. Eine Pipeline, die jede Schmuckfarbe auf DeviceGray herunterstufte, erzeugte graue und weiße Ausgabe, was nicht schwarz ist, sodass die Assertion bestand, während die gesamte Funktion tot war. Schwache Assertions der Form „nicht leer“, „nicht ganz schwarz“ oder „Digest ist ungleich null“ können einen funktionierenden Renderer nicht von einem defekten unterscheiden, denn fast jeder Fehlermodus erzeugt immer noch irgendwelche Pixel
Der Assertion-Stil, der diese Fehler tatsächlich erwischt, fixiert die erwartete Farbe: ein handgebautes minimales PDF rendern, dessen Separation-Druckfarbe zu einem bekannten Farbton aufgelöst wird, und dann die Pixel zählen, in denen dieser Farbton dominiert. Das Rendern in eine Bitmap zur Prüfung verwendet denselben Einstiegspunkt RenderLoadedPageToBitmap, der in dem Leitfaden zum Rendern von Seiten in Bitmaps beschrieben wird
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;
// Die erwartete Druckfarbe fixieren: eine echte Fläche rot-dominanter
// Pixel verlangen, sich nie mit "nicht ganz schwarz" zufriedengeben.
Assert(RedHits > 500);
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Die Testdatei ist ebenso wichtig wie die Assertion. Ein handgebautes PDF von wenigen hundert Bytes, mit einer Separation-Füllung und sonst nichts, lässt keine Zweifel daran, was die erwartete Ausgabe ist; eine reale Datei übt mehr Code aus, kann Ihnen aber nicht sagen, welche Stufe versagt hat. Wir betrachten das Zählen von Pixeln der erwarteten Farbe inzwischen als Mindestanforderung für jeden Rendering-Smoke-Test, denn es ist der einzige Assertion-Stil, der diese Defekte ans Licht gezwungen hat
Das Rendering von Schmuckfarben und CIE-Farbräumen ist Teil der Pipeline für geladene Dokumente in der HotPDF Delphi Component für Delphi und C++Builder, neben der Funktions-Engine, dem Shading-Rendering und den oben gezeigten Bitmap-Exportpfaden