Eingebundene Bilder wurden oft unabhängig vom restlichen Dokument erstellt, und haben ihre eigenen Farbkorrekturinformationen. Darum ist es in PDF möglich, die Farbkorrektur eingebundener Bilder separat einzustellen. Zudem kann man für die allfällige Konversion von RGB nach CMYK festlegen, ob möglichst auf exakte Farbumsetzung, Erhaltung der Farbsättigung, oder Erhaltung des photographischen Eindrucks geachtet werden soll.
Um eine vom Dokumentstandard abweichende Farbkorrektur für das Bild festzulegen, müssen wir im Bildobjekt den /ColorSpace
Eintrag ändern. Dieser war bisher auf /DeviceGray
, /DeviceRGB
, /DeviceCMYK
oder ein Array mit einem /Indexed
Farbmodell.
/DeviceGray
, /DeviceRGB
oder /DeviceCMYK
werden einfach durch eine Umrechnungstabelle oder einen Verweis auf ein ICC-Profil ersetzt.
Beispiel:
10 0 obj << /Type /XObject /Subtype /Image /Width 800 /Height 600 /ColorSpace [/ICCBased 11 0 R] /BitsPerComponent 8 /Interpolate true /Filter [/ASCII85Decode /DCTDecode] /Length 35654 >> stream ...~> endstream endobj
Bei indizierten Farbräumen ist die Sache ein klein wenig kniffliger. Diese sind bekanntlich als Arrays angegeben, von denen der erste Eintrag /Indexed
ist. Der zweite Eintrag war in unserem Fall bisher immer /DeviceRGB
. Diesen Eintrag müssen wir ersetzen.
Beispiel:
/ColorSpace [/Indexed [/ICCBased 11 0 R] 3 <FF000000FF000000FFFFFFFF>]
Vorsicht: Beim Alphakanal muss /ColorSpace
auf /DeviceGray
gesetzt werden. /CalGray
und /ICCBased
sind hier nicht erlaubt (und machen auch wenig Sinn).
Das Dictionary des Bildobjekts kann einen Eintrag /Intent
haben, der auf die bekannten Intents gesetzt werden kann. Als Daumenregel kann man sich merken:
Bei Formaten, die typischerweise Fotos enthalten (insbesondere JPEG), eignet sich /Perceptual
als Standard.
Bei allen anderen Formaten eignet sich /RelativeColorimetric
als Standard.
Die Umrechnungspriorität müssen wir von Hand einstellen. Farbkorrekturinformationen hingegen sind zuweilen in den Bilddateien abgelegt. Wir müssen sie nur rausfischen.
Bei JPEG herrscht leider ein gewisses Chaos. Das JFIF Format enthält überhaupt keine Farbkorrekturinformationen. Das EXIF Format erlaubt zwar, solche abzulegen, jedoch sind diese unzuverlässig: Zum einen handelt es sich meist nur um Schätzwerte, und zum anderen oft um die Werte der Kamera. Wir brauchen aber die Werte des Bildschirms, an dem das Bild aufbereitet wurde.
ICC hat schliesslich eine eigene Erweiterung veröffentlicht, welche es ermöglicht, ein ICC-Profil in eine JPEG-Datei einzubetten. Um diese Information zu ermitteln, müssen wir alle Metadatenblöcke des JPEG anschauen. Zur Erinnerung: Sobald wir dabei auf einen Block mit dem Marker <FF DA> stossen, können wir die Verarbeitung abbrechen. Nach diesem Block folgend die komprimierten Bilddaten.
Das ICC-Profil ist in einem oder mehreren Metadatenblöcken abgelegt. Diese Blöcke haben den Marker <FF E2>. Der Aufbau ist wie folgt:
Position | Grösse | Typ | Wert |
---|---|---|---|
0 | 1 Byte | uint8 | immer 0xFF |
1 | 1 Byte | uint8 | immer 0xE2 |
2 | 2 Byte | uint16 | Blockgrösse |
4 | 11 Byte | char | ASCII-String ICC_PROFILE |
15 | 1 Byte | uint8 | immer 0 |
16 | 1 Byte | uint8 | Blocknummer |
17 | 1 Byte | uint8 | Blockanzahl |
18 | Blockgrösse - 16 | * | ICC-Profildaten |
Der String ICC_PROFILE
am Anfang soll den Block zweifelsfrei als ICC-Block markieren. Es können auch andere Daten in einem Block mit dem <FF E2> Marker abgelegt sein, da es sich nicht um einen offiziellen Standard des JPEG Komitees handelt.
Ist die Blockanzahl 1, so können wir die Profildaten aus diesem Block 1:1 übernehmen. Ansonsten müssen wir die Profildaten aller ICC-Blocks aneinanderhängen, und zwar in der Reihenfolge, die mit der Blocknummer vorgegeben ist (aufsteigend ab 1).
Die (allenfalls zusammengesetzten) Profildaten sind die unveränderten Daten einer ICC-Datei. Das heisst, die Daten können in einen Stream gepackt werden, der dann mit /ICCBased
referenziert werden kann (den /N
Eintrag im Stream des ICC-Profils nicht vergessen).
PNG bietet drei Möglichkeiten, die Farbkorrektur anzugeben: sRGB-Deklaration, ICC-Profile und Chromatizitätsdeklaration.
Das ist die einfachste Methode. Enthält das PNG einen Block mit Marker sRGB
(auf Gross- und Kleinschreibung achten), so wird das sRGB-Farbmodell verwendet. Man kann dementsprechend /CalRGB
bzw. /CalGray
mit den zu sRGB passenden Zahlen verwenden. Falls dieser Block vorkommt, sollten ICC-Profile oder Chromatizitätsdeklarationen ignoriert werden.
Position | Grösse | Typ | Wert |
---|---|---|---|
0 | 4 Byte | uint32 | Blockgrösse |
4 | 4 Byte | char | immer sRGB |
8 | 1 Byte | uint8 | Umrechnungspriorität |
9 | 4 Byte | uint32 | Prüfsumme |
Wie man sieht, kann in diesem Block (und nur in diesem Block) eine bevorzugte Umrechnungspriorität definiert werden. Folgende vier Werte sind möglich:
0 | /Perceptual |
---|---|
1 | /RelativeColorimetric |
2 | /Saturation |
3 | /AbsoluteColorimetric |
Enthält das PNG einen Block mit Marker iCCP
, so enthält dieser ein ICC-Profil. Der Blockaufbau ist wie folgt:
Position | Grösse | Typ | Wert |
---|---|---|---|
0 | 4 Byte | uint32 | Blockgrösse |
4 | 4 Byte | char | immer iCCP |
8 | * | char | Profilname |
* | 1 Byte | uint8 | immer 0 |
* | 1 Byte | uint8 | Kompression |
* | * | * | ICC-Profildaten |
* | 4 Byte | uint32 | Prüfsumme |
Der Profilname ist in ISO Latin-1 kodiert, für uns aber nicht wichtig. Entscheidend ist, dass der Name selbst kein 0-Byte enthalten kann. Das erste 0-Byte innerhalb der Blockdaten ist folglich der oben erwähnte uint8 mit Wert 0. Das ist die einzige Möglichkeit, die Länge des Profilnamens zu ermitteln.
Die Kompressionsmethode ist immer 0, was Deflate bedeutet. Das heisst, wir können die Profildaten 1:1 in einen Stream packen, und den /FlateDecode
Filter angeben. Den Stream können wir dann mit /ICCBased
referenzieren.
Die ist die letzte Möglichkeit, falls weder eine sRGB-Deklaration noch ein ICC-Profil vorhanden sind. In diesem Fall haben wir zwei Blöcke. Einen cHRM
Block mit Weisspunkt und Matrix, und einen gAMA
Block mit dem Gammawert. Falls nur einer der beiden vorhanden ist (was nicht der Fall sein sollte), so kann man die sRGB-Werte für den fehlenden Block verwenden.
Position | Grösse | Typ | Wert |
---|---|---|---|
0 | 4 Byte | uint32 | Blockgrösse |
4 | 4 Byte | char | immer gAMA |
8 | 4 Byte | uint32 | Gammawert (invers) |
12 | 4 Byte | uint32 | Prüfsumme |
Der inverse Gammawert ist eine Ganzzahl auf einer Skala von 0 - 100'000. Den Wert für das PDF erhalten wir folglich, indem wir mit einer Fliesskommadivision 100'000 durch den Wert aus dem PNG teilen:
$$g_{pdf} = \frac{100000}{g_{png}}$$
Der cHRM
Block enthält die Chromatizität des Weisspunkts und der verwendeten Rot- Grün- und Blautöne. Dies ist eine weitere CIE-Norm, die mit zwei Koordinaten x und y ausgedrückt wird. Um Verwechslungen mit dem XYZ-Farbraum zu vermeiden, werden für Chromatizität immer Kleinbuchstaben, für XYZ immer Grossbuchstaben verwendet.
Position | Grösse | Typ | Wert |
---|---|---|---|
0 | 4 Byte | uint32 | Blockgrösse |
4 | 4 Byte | char | immer cHRM |
8 | 4 Byte | uint32 | xw |
12 | 4 Byte | uint32 | yw |
16 | 4 Byte | uint32 | xr |
20 | 4 Byte | uint32 | yr |
24 | 4 Byte | uint32 | xg |
28 | 4 Byte | uint32 | yg |
32 | 4 Byte | uint32 | xb |
36 | 4 Byte | uint32 | yb |
40 | 4 Byte | uint32 | Prüfsumme |
Ähnlich wie im gAMA
Block handelt es sich um vorzeichenlose Ganzzahlen auf einer Skala von 0 - 100'000. Anders als bei gAMA
sind sie aber nicht invers. Folglich müssen wir diesmal die Zahlen mit Fliesskommadivisionen durch 100'000 teilen.
$$x_{pdf} = \frac{x_{png}}{100000}$$ $$y_{pdf} = \frac{y_{png}}{100000}$$
Wenn wir dies erledigt haben, können wir die Werte in den XYZ-Farbraum umrechnen.
Zunächst müssen wir die XYZ-Werte des Weisspunkts berechnen:
$$X_w = \frac{x_w}{y_w}$$ $$Y_w = 1$$ $$Z_w = \frac{1 - x_w - y_w}{y_w}$$
Für die Grundfarben brauchen wir folgenden Formelsatz:
$$z_r = 1 - x_r - y_r$$ $$z_g = 1 - x_g - y_g$$ $$z_b = 1 - x_b - y_b$$ $$\begin{bmatrix} S_r && S_g && S_b \end{bmatrix} = \begin{bmatrix} x_r && y_r && z_r \\\\ x_g && y_g && z_g \\\\ x_b && y_b && z_b \end{bmatrix}^{-1} \begin{bmatrix} X_w && Y_w && Z_w \end{bmatrix}$$ $$\begin{bmatrix} X_r && Y_r && Z_r \end{bmatrix} = \begin{bmatrix} x_r && y_r && z_r \end{bmatrix} S_r$$ $$\begin{bmatrix} X_g && Y_g && Z_g \end{bmatrix} = \begin{bmatrix} x_g && y_g && z_g \end{bmatrix} S_g$$ $$\begin{bmatrix} X_b && Y_b && Z_b \end{bmatrix} = \begin{bmatrix} x_b && y_b && z_b \end{bmatrix} S_b$$
Wie man sieht, enthalten die Formeln leider eine invertierte Matrix. Das macht die arithmetische Variante recht kompliziert:
$$z_r = 1 - x_r - y_r$$ $$z_g = 1 - x_g - y_g$$ $$z_b = 1 - x_b - y_b$$ $$m_{xr} = y_gz_b - z_gy_b$$ $$m_{xg} = z_ry_b - y_rz_b$$ $$m_{xb} = y_rz_g - z_ry_g$$ $$d = m_{xr}x_r + m_{xg}x_g + m_{xb}x_b$$ $$S_r = \frac{m_{xr}X_w + (y_bZ_w - z_b) x_g + (z_g - y_gZ_w) x_b}{d}$$ $$S_g = \frac{m_{xg}X_w + (y_rZ_w - z_r) x_b + (z_b - y_bZ_w) x_r}{d}$$ $$S_b = \frac{m_{xb}X_w + (y_gZ_w - z_g)x_r + (z_r - y_rZ_w) x_g}{d}$$ $$X_r = x_rS_r$$ $$Y_r = y_rS_r$$ $$Z_r = z_rS_r$$ $$X_g = x_gS_g$$ $$Y_g = y_gS_g$$ $$Z_g = z_gS_g$$ $$X_b = x_bS_b$$ $$Y_b = y_bS_b$$ $$Z_b = z_bS_b$$
Damit können wir nun die Werte für /CalRGB
zusammenstellen. Ins /WhitePoint
Array, gehören nacheinander die Werte Xw, Yw und Zw. Ins /Matrix
Array nacheinander die Werte Xr, Yr, Zr, Xg, Yg, Zg, Xb, Yb und Zb.
Diskussion