Author: tilman
Date: Wed Aug 12 07:59:01 2026
New Revision: 1937069

Log:
PDFBOX-6235: fix NPE with CMYK images, set decode, add test, as suggested by 
Seungmin Lee

Modified:
   
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java
   
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java

Modified: 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java
==============================================================================
--- 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java
  Wed Aug 12 07:54:01 2026        (r1937068)
+++ 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java
  Wed Aug 12 07:59:01 2026        (r1937069)
@@ -330,10 +330,24 @@ public final class JPEGFactory
         // create XObject
         byte[] encoded = encodeImageToJPEGStream(awtColorImage, quality, dpi);
         ByteArrayInputStream encodedByteStream = new 
ByteArrayInputStream(encoded);
+        PDColorSpace colorSpace = getColorSpaceFromAWT(awtColorImage);
 
         PDImageXObject pdImage = new PDImageXObject(document, 
encodedByteStream, COSName.DCT_DECODE,
                 awtColorImage.getWidth(), awtColorImage.getHeight(), 8,
-                getColorSpaceFromAWT(awtColorImage));
+                colorSpace);
+        if (colorSpace instanceof PDDeviceCMYK)
+        {
+            COSArray decode = new COSArray();
+            decode.add(COSInteger.ONE);
+            decode.add(COSInteger.ZERO);
+            decode.add(COSInteger.ONE);
+            decode.add(COSInteger.ZERO);
+            decode.add(COSInteger.ONE);
+            decode.add(COSInteger.ZERO);
+            decode.add(COSInteger.ONE);
+            decode.add(COSInteger.ZERO);
+            pdImage.setDecode(decode);
+        }
 
         // extract alpha channel (if any)
         BufferedImage awtAlphaImage = getAlphaImage(image);
@@ -392,10 +406,16 @@ public final class JPEGFactory
             IIOMetadata metadata = 
imageWriter.getDefaultImageMetadata(imageTypeSpecifier, jpegParam);
             Element tree = (Element) 
metadata.getAsTree("javax_imageio_jpeg_image_1.0");
             Element jfif = (Element) 
tree.getElementsByTagName("app0JFIF").item(0);
-            String dpiString = Integer.toString(dpi);
-            jfif.setAttribute("Xdensity", dpiString);
-            jfif.setAttribute("Ydensity", dpiString);
-            jfif.setAttribute("resUnits", "1"); // 1 = dots/inch
+            // PDFBOX-6235: JFIF APP0 is only defined for 1- and 3-component 
JPEGs. A 4-component
+            // (CMYK) image gets an Adobe APP14 marker instead, so there is no 
app0JFIF
+            // node and the DPI cannot be expressed in the stream at all.
+            if (jfif != null)
+            {
+                String dpiString = Integer.toString(dpi);
+                jfif.setAttribute("Xdensity", dpiString);
+                jfif.setAttribute("Ydensity", dpiString);
+                jfif.setAttribute("resUnits", "1"); // 1 = dots/inch
+            }
 
             // write
             imageWriter.write(metadata, new IIOImage(image, null, null), 
jpegParam);

Modified: 
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java
==============================================================================
--- 
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java
      Wed Aug 12 07:54:01 2026        (r1937068)
+++ 
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java
      Wed Aug 12 07:59:01 2026        (r1937069)
@@ -15,8 +15,11 @@
  */
 package org.apache.pdfbox.pdmodel.graphics.image;
 
+import java.awt.Color;
 import java.awt.Graphics;
+import java.awt.color.ColorSpace;
 import java.awt.image.BufferedImage;
+
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -36,6 +39,7 @@ import static org.apache.pdfbox.pdmodel.
 import static 
org.apache.pdfbox.pdmodel.graphics.image.ValidateXImage.doWritePDF;
 import static org.apache.pdfbox.pdmodel.graphics.image.ValidateXImage.validate;
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assume.assumeTrue;
 
 /**
  * Unit tests for JPEGFactory
@@ -284,6 +288,38 @@ public class JPEGFactoryTest extends Tes
         checkJpegStream(testResultsDir, "PDFBOX-5196-lotus.pdf", new 
ByteArrayInputStream(ba));        
     }
 
+    // PDFBOX-6235
+    public void testCreateFromImageCMYK() throws IOException
+    {
+        // magick -size 200x200 gradient:red-blue -colorspace CMYK 
PDFBOX-6235-cmyk.jpg
+        InputStream is = 
JPEGFactoryTest.class.getResourceAsStream("PDFBOX-6235-cmyk.jpg");
+        byte[] ba= IOUtils.toByteArray(is);
+        is.close();
+
+        BufferedImage bim = ImageIO.read(new ByteArrayInputStream(ba));
+
+        // This test works only with the original java imaging, not with 
twelvemonkeys
+        assumeTrue(bim.getColorModel().getColorSpace().getType() == 
ColorSpace.TYPE_CMYK);
+        assertEquals(BufferedImage.TYPE_CUSTOM, bim.getType());
+        assertEquals(4, bim.getColorModel().getNumComponents());
+
+        PDDocument document = new PDDocument();
+        PDImageXObject ximage = JPEGFactory.createFromImage(document, bim);
+        validate(ximage, 8, 200, 200, "jpg", PDDeviceCMYK.INSTANCE.getName());
+        // the samples are inverted, so a /Decode array is required
+        assertTrue(Arrays.equals(new float[] { 1, 0, 1, 0, 1, 0, 1, 0 }, 
ximage.getDecode().toFloatArray()));
+
+        // using the one created from the stream is more reliable than using 
"bim"
+        // because of flaws in converting CMYK to RGB
+        // See https://stackoverflow.com/questions/19540064/
+        BufferedImage expected = JPEGFactory.createFromStream(document, new 
ByteArrayInputStream(ba)).getImage();
+
+        float meanAbsDiffPerPixel = computeMeanAbsDiffPerPixel(expected, 
ximage.getImage());
+        assertTrue(meanAbsDiffPerPixel < 1);
+
+        doWritePDF(document, ximage, testResultsDir, "PDFBOX-6235-cmyk.pdf");
+    }
+
     // check whether it is possible to extract the jpeg stream exactly 
     // as it was passed to createFromStream
     private void checkJpegStream(File testResultsDir, String filename, 
InputStream expected)
@@ -298,4 +334,30 @@ public class JPEGFactoryTest extends Tes
         dctStream.close();
         doc.close();
     }
+
+    private float computeMeanAbsDiffPerPixel(BufferedImage expected, 
BufferedImage actual)
+    {
+        // assumption: both sizes are identical
+        int w = expected.getWidth();
+        int h = expected.getHeight();
+        long sum = 0;
+        long count = 0;
+        for (int y = 0; y < h; y++)
+        {
+            for (int x = 0; x < w; x++)
+            {
+                count += 3;
+                Color expectedRGB = new Color(expected.getRGB(x, y));
+                Color actualRGB = new Color(actual.getRGB(x, y));
+                if (expectedRGB == actualRGB)
+                {
+                    continue;
+                }
+                sum += Math.abs(expectedRGB.getRed() - actualRGB.getRed());
+                sum += Math.abs(expectedRGB.getGreen() - actualRGB.getGreen());
+                sum += Math.abs(expectedRGB.getBlue() - actualRGB.getBlue());
+            }
+        }
+        return sum / (float) count;
+    }
 }

Reply via email to