Author: tilman
Date: Mon Sep 14 13:24:12 2026
New Revision: 1938201

Log:
PDFBOX-6260: add chromatic adaptation, by Valery Bokov and Claude Code

Modified:
   
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDCIEDictionaryBasedColorSpace.java

Modified: 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDCIEDictionaryBasedColorSpace.java
==============================================================================
--- 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDCIEDictionaryBasedColorSpace.java
       Mon Sep 14 13:24:09 2026        (r1938200)
+++ 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/color/PDCIEDictionaryBasedColorSpace.java
       Mon Sep 14 13:24:12 2026        (r1938201)
@@ -33,6 +33,20 @@ public abstract class PDCIEDictionaryBas
 
     private static final ColorSpace CIEXYZ = 
ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);
 
+    // java.awt.color.ColorSpace(CS_CIEXYZ) treats its input as relative to 
the D50 profile
+    // connection space (verified: XYZ(0.9642 1.0 0.8249) is the only 
whitepoint that round-trips
+    // to RGB(1,1,1)). A CalRGB/CalGray/Lab dictionary can declare any other 
whitepoint, so the
+    // XYZ computed from it must be chromatically adapted to D50 first, or 
non-D50 whitepoints
+    // pick up a uniform color cast - see PDFBOX-6260, where whitepoint 
(0.5505 1.0 1.989) made
+    // pure white input render as cyan and mid-gray as a cyan-blue.
+    private static final double[][] BRADFORD = {
+        { 0.8951000,  0.2664000, -0.1614000},
+        {-0.7502000,  1.7135000,  0.0367000},
+        { 0.0389000, -0.0685000,  1.0296000}
+    };
+    private static final double[][] BRADFORD_INV = invert(BRADFORD);
+    private static final double[] PCS_WHITE_D50 = {0.9642, 1.0, 0.8249};
+
     // we need to cache whitepoint values, because using getWhitePoint()
     // would create a new default object for each pixel conversion if the 
original
     // PDF didn't have a whitepoint array
@@ -40,6 +54,10 @@ public abstract class PDCIEDictionaryBas
     protected float wpY = 1;
     protected float wpZ = 1;
 
+    // chromatic adaptation matrix from this space's whitepoint to the D50 PCS 
white, cached
+    // per color space instance (not per pixel) since it only depends on the 
declared whitepoint
+    private float[] chromaticAdaptation;
+
     protected PDCIEDictionaryBasedColorSpace(COSName cosName)
     {
         array = new COSArray();
@@ -68,31 +86,102 @@ public abstract class PDCIEDictionaryBas
         wpX = whitepoint.getX();
         wpY = whitepoint.getY();
         wpZ = whitepoint.getZ();
+        chromaticAdaptation = adaptationMatrix(new double[] { wpX, wpY, wpZ }, 
PCS_WHITE_D50);
     }
 
     protected float[] convXYZtoRGB(float x, float y, float z)
     {
+        float ax = chromaticAdaptation[0] * x + chromaticAdaptation[1] * y + 
chromaticAdaptation[2] * z;
+        float ay = chromaticAdaptation[3] * x + chromaticAdaptation[4] * y + 
chromaticAdaptation[5] * z;
+        float az = chromaticAdaptation[6] * x + chromaticAdaptation[7] * y + 
chromaticAdaptation[8] * z;
+
         // toRGB() malfunctions with negative values
         // XYZ must be non-negative anyway:
         // 
http://ninedegreesbelow.com/photography/icc-profile-negative-tristimulus.html
-        if (x < 0)
+        if (ax < 0)
         {
-            x = 0;
+            ax = 0;
         }
-        if (y < 0)
+        if (ay < 0)
         {
-            y = 0;
+            ay = 0;
         }
-        if (z < 0)
+        if (az < 0)
         {
-            z = 0;
+            az = 0;
         }
         return CIEXYZ.toRGB(new float[]
         {
-            x, y, z
+            ax, ay, az
         });
     }
 
+    private static float[] adaptationMatrix(double[] srcWhite, double[] 
dstWhite)
+    {
+        double[] srcCone = multiply(BRADFORD, srcWhite);
+        double[] dstCone = multiply(BRADFORD, dstWhite);
+        double[][] scale = {
+            { dstCone[0] / srcCone[0], 0, 0 },
+            { 0, dstCone[1] / srcCone[1], 0 },
+            { 0, 0, dstCone[2] / srcCone[2] }
+        };
+        double[][] m = multiply(multiply(BRADFORD_INV, scale), BRADFORD);
+        return new float[]
+        {
+            (float) m[0][0], (float) m[0][1], (float) m[0][2],
+            (float) m[1][0], (float) m[1][1], (float) m[1][2],
+            (float) m[2][0], (float) m[2][1], (float) m[2][2]
+        };
+    }
+
+    private static double[] multiply(double[][] m, double[] v)
+    {
+        return new double[]
+        {
+            m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
+            m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
+            m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2]
+        };
+    }
+
+    private static double[][] multiply(double[][] a, double[][] b)
+    {
+        double[][] r = new double[3][3];
+        for (int i = 0; i < 3; i++)
+        {
+            for (int j = 0; j < 3; j++)
+            {
+                r[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * 
b[2][j];
+            }
+        }
+        return r;
+    }
+
+    private static double[][] invert(double[][] m)
+    {
+        double det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
+                   - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+                   + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
+        return new double[][]
+        {
+            {
+                (m[1][1] * m[2][2] - m[1][2] * m[2][1]) / det,
+                -(m[0][1] * m[2][2] - m[0][2] * m[2][1]) / det,
+                (m[0][1] * m[1][2] - m[0][2] * m[1][1]) / det
+            },
+            {
+                -(m[1][0] * m[2][2] - m[1][2] * m[2][0]) / det,
+                (m[0][0] * m[2][2] - m[0][2] * m[2][0]) / det,
+                -(m[0][0] * m[1][2] - m[0][2] * m[1][0]) / det
+            },
+            {
+                (m[1][0] * m[2][1] - m[1][1] * m[2][0]) / det,
+                -(m[0][0] * m[2][1] - m[0][1] * m[2][0]) / det,
+                (m[0][0] * m[1][1] - m[0][1] * m[1][0]) / det
+            }
+        };
+    }
+
     /**
      * This will return the whitepoint tristimulus. As this is a required field
      * this will never return null. A default of 1,1,1 will be returned if the

Reply via email to