HotPDF, die native VCL-PDF-Komponente für Delphi und C++Builder, wertet die drei PDF-Funktionstypen aus, die auf Formeln statt auf Stichproben-Rastern basieren: Typ-2-Exponentialinterpolation, Typ-3-Stitching und Typ-4-PostScript-Rechnerfunktionen, entsprechend ISO 32000-1 §7.10.3, §7.10.4 und §7.10.5. Typ 2 blendet entlang einer Kurve zwischen zwei Ausgabevektoren, Typ 3 verkettet mehrere Teilfunktionen über einer einzigen Eingabedomäne, und Typ 4 führt ein eingeschränktes PostScript-Programm aus, das verzweigen, vergleichen und nahezu alles berechnen kann, was ein Content-Stream aus seinen Eingaben benötigt. Wird auch nur einer der drei leicht falsch implementiert, kündigt sich der Fehler nie als Bug an – er zeigt sich als Verlauf mit einem toten flachen Band, als Schmuckfarbe, die reines Schwarz rendert, oder als Rechnerfunktion, die genau bei den Eingaben um eins daneben liegt, die eine Testsuite zufällig nicht ausprobiert hat
Diese drei stehen neben einem vierten, Typ 0, der ein abgetastetes Raster statt einer Formel speichert und separat in dem begleitenden Artikel zu Typ-0-Farb-Lookup-Tabellen behandelt wird. Beide Familien lösen dasselbe Problem, eine Eingabe auf eine Ausgabe abzubilden, aber Typ 0 sind einmal berechnete und in die Datei eingebackene Daten, während Typ 2, 3 und 4 Code sind, den der Reader bei jedem Aufruf auswertet. Alle vier teilen sich einen Dispatch-Punkt in HotPDFs Renderer, indiziert über den Eintrag /FunctionType des Funktionswörterbuchs, sodass eine Schattierung, eine Farbtontransformation oder eine Halbton-Schmuckfarbfunktion nie wissen muss, welchen der vier Typen sie erhalten hat, bevor sie nach einer Farbe fragen kann
Wie funktioniert eine PDF-Typ-2-Exponentialfunktion?
Eine PDF-Typ-2-Funktion berechnet eine einzige Formel — y = C0 + x^N × (C1 − C0), komponentenweise angewendet — wobei x die einzige Eingabe der Funktion ist, normalisiert gegen ihre /Domain, bevor die Formel läuft (ISO 32000-1 §7.10.3). /C0 und /C1 sind die Ausgabevektoren an den beiden Enden dieses Bereichs, eine Zahl pro Ausgabekomponente, und /N ist der Exponent, der die Kurve dazwischen formt: N = 1 ergibt die geradlinige Rampe hinter den meisten Verlaufsstopps und Duoton-Konvertierungen, N über 1 zieht die Kurve zu C0, und N zwischen 0 und 1 drückt sie zu C1. RegisterExponentialFunction baut dieses Wörterbuch aus fünf Argumenten und gibt ein Funktionsobjekt zurück, das bereit ist, in eine Schattierung, eine Halbton-Schmuckfarbfunktion oder überall sonst eingesetzt zu werden, wo die Spezifikation einen /Function-Schlüssel akzeptiert
Die Komponentenanzahl-Beziehung zwischen C0 und C1 zählt gleich zweimal: einmal, wenn Sie eine Typ-2-Funktion autorisieren, und erneut, wann immer HotPDF eine rendern muss, die es nicht selbst erzeugt hat. Auf der Autorenseite prüft RegisterExponentialFunction C0 und C1 gegeneinander und wirft eine Exception, falls sie nicht übereinstimmen, sodass ein Aufruf, der BeginDoc erreicht, bereits ein in sich konsistentes Funktionsobjekt hat. Auf der Renderseite muss der Evaluator jedoch vertrauen, welche /C0- und /C1-Arrays eine Quelldatei tatsächlich deklariert – etwa eine zur Vorschau geöffnete Datei aus einer Druckerei, oder ein signiertes Dokument, das einem Benutzer angezeigt wird –, und Versionen vor 2.376.0 lasen diese Arrays in einen Puffer, der für vier Komponenten dimensioniert war, den CMYK-Fall. Ein DeviceGray- oder DeviceRGB-Exponential-Farbton mit einem ein- oder dreielementigen /C0 und /C1 scheiterte bei diesem Lesevorgang still, und beide Arrays blieben bei null, sodass der Farbton flach schwarz statt der beabsichtigten Farbe malte. Version 2.376.0 dimensionierte den Reader auf die tatsächlich deklarierte Ausgabeanzahl der Funktion um, statt auf einen festen Puffer – genau die Art von Bug, die nur ein Nicht-CMYK-Testfall aufdeckt, da die bestehende Testsuite durchgehend CMYK lief, wo vier-in-vier immer passte
var
EaseIn: THPDFDictionaryObject;
begin
// Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
EaseIn := Pdf.RegisterExponentialFunction(
[0,1], // Domain: single input, clamped to [0,1]
[0, 0, 0], // C0: output at x = 0
[0.8, 0, 0], // C1: output at x = 1
3, // N: exponent, 1 = linear, > 1 eases toward C0
[]); // Range omitted: defaults to a [0,1] clamp per output
end;
Typ-3-Stitching: Teilfunktionen über ein Bounds-Array verketten
Eine PDF-Typ-3-Funktion verkettet (stitcht) k Teilfunktionen zu einer stückweisen Abbildung über die /Domain einer einzigen Eingabe, und die beiden Arrays, die das ermöglichen, sind /Bounds und /Encode (ISO 32000-1 §7.10.4). /Bounds hält k − 1 innere Trennpunkte, die /Domain in k aufeinanderfolgende Intervalle zerlegen; der Evaluator wählt das erste Intervall, dessen obere Grenze die Eingabe übersteigt, oder das letzte Intervall, sobald die Eingabe die letzte Grenze erreicht, und übergibt an die Teilfunktion dieses Intervalls. /Encode bildet dann die Eingabe von ihrer Position innerhalb dieses Intervalls auf den Eingabebereich ab, den die gewählte Teilfunktion selbst erwartet — typischerweise [0, 1], falls die Teilfunktion ein weiteres Exponentialsegment ist —, bevor die Auswertung eine Ebene tiefer in der eigenen /Domain und /Range dieser Teilfunktion fortgesetzt wird
HotPDFs Stitching-Evaluator behandelte früher nur genau zwei Teilfunktionen, und sein /Bounds-Reader erforderte ein vollständiges Acht-Elemente-Array, sodass der eine Trennpunkt, den ein zweisegmentiger Verlauf tatsächlich braucht — eine einzige Zahl in /Bounds — immer beim Parsen scheiterte und die Funktion nichts zurückgab. /Encode wurde überhaupt nicht angewendet. Version 2.376.0 schrieb die Auswahl als die allgemeine k-Teilfunktions-Suche neu, die die Spezifikation beschreibt, und begann, /Bounds gegen seine tatsächlich deklarierte Länge zu lesen, sodass ein aus so vielen Exponentialsegmenten gestitchter Drei-, Vier- oder Fünf-Stopp-Verlauf jetzt genauso aufgelöst wird, wie ein Zwei-Segment-Verlauf es immer schon behauptet hatte. Das folgende Beispiel baut eine zweisegmentige Schwarz-über-Rot-zu-Weiß-Rampe, die Form, zu der eine axiale oder radiale Schattierung greift, wann immer eine einzelne Exponentialkurve nicht jeden Farbstopp tragen kann, den ein Design verlangt
var
ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
// Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
ToRed := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0], [0.8, 0, 0], 1, []);
ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1], 1, []);
Ramp := Pdf.RegisterStitchingFunction(
[0,1], // Domain: the stitched function's own input range
[ToRed, ToWhite], // Functions: k = 2 sub-functions
[0.5], // Bounds: k - 1 = 1 split point
[0,1, 0,1], // Encode: 2 numbers per sub-function
[]); // Range omitted: inherited from each sub-function
end;
Was kann eine Typ-4-PostScript-Rechnerfunktion, das Typ 2 und 3 nicht können?
Eine PDF-Typ-4-Funktion führt ein echtes, wenn auch absichtlich eingeschränktes, Programm aus: einen PostScript-Rechner, der seine Eingaben auf einen Operanden-Stack legt, arithmetische, Vergleichs-, Stack-Manipulations- und boolesche Operatoren sowie if/ifelse-Bedingungen ausführt und seine Ausgaben am Ende auf dem Stack hinterlässt (ISO 32000-1 §7.10.5, Tabelle 42). Es gibt keine Schleifenkonstruktion und keinen benannten Variablenspeicher, nur den Stack, was ein konformes Programm leicht nachvollziehbar hält — aber innerhalb dieses eingeschränkten Operatorsatzes kann Typ 4 Dinge ausdrücken, die Typ 2 und Typ 3 nicht können, etwa eine echte Mehrfarben-Mischformel für eine DeviceN-Separation oder eine Halbton-Schmuckfarbfunktion mit einem bedingten Schwellenwert. HotPDFs Evaluator, HPDFEvalPostScriptCalculator, tokenisiert das Programm einmal — Zahlen, Operatoren und { }-Prozedurblöcke — und durchläuft dann einen Operanden-Stack mit 100 Einträgen, die Tiefe, die ISO 32000-1 §7.10.5 verlangt, hinter einer harten Obergrenze von 50.000 ausgewerteten Operatoren als defensive Rückfallsicherung gegen pathologische oder handgeschriebene Programme
Der roll-Operator: die Richtung ist leicht verkehrt herum
roll ist der Operator, der beim ersten Versuch am ehesten verkehrt herum herauskommt, weil sowohl seine Argumentreihenfolge als auch seine Rotationsrichtung beide entgegen der Art laufen, wie man sie auf Deutsch beschreiben würde. n j roll nimmt eine Anzahl n und einen Rotationsbetrag j vom Stack, verschiebt dann die obersten n Stack-Einträge zyklisch um j Positionen, wobei Elemente, die an einem Ende herausfallen, am anderen Ende wieder eingesetzt werden; das kanonische Beispiel, direkt aus der Spezifikation, ist a b c 3 1 roll, das c a b erzeugt — das oberste Element wandert an den Boden der Gruppe, nicht umgekehrt, und jedes andere Element rückt um eins nach oben, um Platz zu machen. HotPDFs Evaluator berechnet die neue Position des Stack-Eintrags i als (i + j) mod n, was genau zu diesem Beispiel passt, aber es ist eine Zwei-Zeilen-Schleife, die sich ebenso leicht mit umgekehrter Rotation schreiben lässt, und ein gespiegeltes roll erzeugt immer noch eine plausibel aussehende Farbe — nur eben nicht die Farbe, um die der Dateiautor gebeten hat
const
Prog = '{ 3 1 roll }'; // (a b c) -> (c a b): the third input moves to the front
var
Reorder: THPDFStreamObject;
begin
// Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
Reorder := Pdf.RegisterPostScriptFunction(
[0,1, 0,1, 0,1], // Domain: 2 numbers per input
[0,1, 0,1, 0,1], // Range: 2 numbers per output (required for Type 4)
Prog);
end;
round ist nicht Delphis Round: kaufmännische versus Bankers-Rundung
Der PostScript-Operator round löst einen .5-Gleichstand jedes Mal zur größeren Ganzzahl hin auf, und Delphis eingebaute Funktion Round tut das nicht: Sie rundet half-to-even, die Bankers-Rundungskonvention, die abwechselt, in welche Richtung ein .5-Gleichstand fällt, damit wiederholtes Runden keinen systematischen Bias anhäuft. Die beiden stimmen fast überall überein und weichen genau an der hier relevanten Grenze voneinander ab — Delphis Round(0.5) gibt 0 zurück und Round(2.5) gibt 2 zurück, während die PDF-Spezifikation für round bei denselben Eingaben 1 und 3 verlangt —, sodass sich die Abweichung durch beiläufiges Testen verbirgt und sich dann als konsistentes Off-by-one überall dort reproduziert, wo die Zwischenrechnung eines Rechnerprogramms genau auf einer halben Ganzzahl landet. ISO 32000-1 §7.10.5 Tabelle 42 ist ausdrücklich: round schiebt einen Bruchteil von .5 zur größeren Ganzzahl, daher implementiert HotPDF den Operator als Floor(x + 0.5) statt Delphis Round aufzurufen, und jeder Code, der die Arithmetik eines Typ-4-Programms von Hand nachbaut oder stichprobenartig prüft, braucht dieselbe Ersetzung
function PostScriptRound(const X: Double): Double;
begin
// ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
// integer. Delphi's Round() is banker's rounding and disagrees here:
// Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
Result := Floor(X + 0.5);
end;
Validierung zur Registrierungszeit fängt ein fehlerhaftes Rechnerprogramm früh ab
Ein fehlerhaftes Typ-4-Programm ist zur Autorenzeit billig zu erkennen und überall sonst teuer, daher speichert RegisterPostScriptFunction nicht nur den Quelltext: Es wertet das Programm einmal probeweise am Mittelpunkt der deklarierten /Domain aus, bevor das Funktionsobjekt überhaupt in das Dokument geschrieben wird. Unausgeglichene { }-Blöcke, ein nicht erkannter Operator, ein Stack-Unterlauf oder eine Ausgabeanzahl, die nicht zu /Range passt, schlagen alle bei diesem Probelauf fehl und lösen sofort eine Exception aus, wobei der Call-Stack auf den RegisterPostScriptFunction-Aufruf zeigt statt auf ein Render-Artefakt, das während der QA an einer bereits ausgelieferten Datei entdeckt wird. Der Mittelpunkt-Probelauf beweist nicht, dass das Programm über seine gesamte /Domain hinweg korrekt ist — eine bedingte Verzweigung, die sich nur nahe einer Kante des Eingabebereichs falsch verhält, kann trotzdem an einem einzigen Stichprobenpunkt vorbeirutschen —, aber er schließt die gesamte Klasse von Programmen aus, die strukturell defekt sind, statt nur in einer Ecke falsch zu liegen
Wo Verläufe und Schmuckfarben diese Funktionen einsetzen
Typ 2, 3 und 4 erscheinen in einem echten PDF selten isoliert; sie tauchen überall dort auf, wo die Spezifikation einen /Function-Schlüssel akzeptiert, und die zwei häufigsten Konsumenten sind Schattierungen und Farbton-Transformationen für Schmuckfarben. Der sh-Operator eines axialen oder radialen Verlaufs (ISO 32000-1 §8.7.4.5) wertet seine /Function einmal pro Position entlang der Verlaufsachse aus, genau der Mehrfachstopp-Fall, für den Typ-3-Stitching existiert. Die Farbton-Transformation eines Separation- oder DeviceN-Farbraums ist der andere häufige Einsatzort für diese drei Typen, und dort verdient sich Typ 4 seinen Nutzen: Eine einzelne Schmuckfarbe reduziert sich meist auf eine Typ-2- oder Typ-0-Kurve, aber eine DeviceN-Mischung mehrerer Farben mit echtem Trapping- und Überdruckverhalten braucht oft die bedingte Logik, die nur ein PostScript-Rechner ausdrücken kann, der Fall, der im Artikel zum Rendern von Separation- und DeviceN-Schmuckfarben behandelt wird. RegisterSeparationFunc ist der passende Aufruf auf der Autorenseite: Er nimmt einen Farbmittelnamen, einen alternativen Farbraum und jedes von der Register*Function-Familie zurückgegebene Objekt entgegen und verdrahtet diese Farbton-Transformation in eine Separation-Farbraum-Ressource, die der Rest der Seite mit scn/SCN auswählen kann
Zusammen decken Typ 0s abgetastete Raster und diese drei formelgetriebenen Typen jede /Function ab, die ein PDF deklarieren kann, und die Wahl des richtigen ist meist eine Frage dessen, was bereits vorliegt: Eine anderswo berechnete Lookup-Tabelle wird zu Typ 0, eine Zwei-Endpunkt-Blende wird zu Typ 2, mehrere über eine Domäne verkettete Blenden werden zu Typ 3, und alles mit echter bedingter Logik wird zu Typ 4. RegisterExponentialFunction, RegisterStitchingFunction und RegisterPostScriptFunction sind Teil der Standard-HotPDF-Komponente für Delphi und C++Builder, zusammen mit dem Rest ihrer ISO-32000-1-Funktions- und Schattierungs-API