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;
+ }
}