Author: tilman
Date: Wed Aug 12 07:59:05 2026
New Revision: 1937070
Log:
PDFBOX-6235: fix NPE with CMYK images, set decode, add test, as suggested by
Seungmin Lee
Modified:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java
Modified:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java
==============================================================================
---
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java
Wed Aug 12 07:59:01 2026 (r1937069)
+++
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactory.java
Wed Aug 12 07:59:05 2026 (r1937070)
@@ -311,10 +311,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);
@@ -372,10 +386,16 @@ public final class JPEGFactory
IIOMetadata data =
imageWriter.getDefaultImageMetadata(imageTypeSpecifier, jpegParam);
Element tree = (Element)
data.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(data, new IIOImage(image, null, null),
jpegParam);
Modified:
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java
==============================================================================
---
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java
Wed Aug 12 07:59:01 2026 (r1937069)
+++
pdfbox/branches/3.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java
Wed Aug 12 07:59:05 2026 (r1937070)
@@ -15,7 +15,9 @@
*/
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;
@@ -45,6 +47,7 @@ import static org.junit.jupiter.api.Asse
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -321,6 +324,40 @@ class JPEGFactoryTest
checkJpegStream(TESTRESULTSDIR, "PDFBOX-5196-lotus.pdf", new
ByteArrayInputStream(ba));
}
+ // PDFBOX-6235
+ @Test
+ void testCreateFromImageCMYK() throws IOException
+ {
+ // magick -size 200x200 gradient:red-blue -colorspace CMYK
PDFBOX-6235-cmyk.jpg
+ byte[] ba;
+ try (InputStream is =
JPEGFactoryTest.class.getResourceAsStream("PDFBOX-6235-cmyk.jpg"))
+ {
+ ba = IOUtils.toByteArray(is);
+ }
+ 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
+ assertArrayEquals(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)
@@ -336,4 +373,30 @@ class JPEGFactoryTest
}
}
}
+
+ 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;
+ }
}