[
https://issues.apache.org/jira/browse/PDFBOX-6235?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18103896#comment-18103896
]
Seungmin Lee commented on PDFBOX-6235:
--------------------------------------
Thanks for taking the time to reproduce this, and for the sample file.
You are right that the null check alone is not a fix — I reproduced the wrong
colors you got, and the cause is a second, separate omission in the same file.
h3. Reproducing your result
Using [^PDFBOX-6235-cmyk.jpg], stock JDK ImageIO (no TwelveMonkeys), PDFBox
3.0.7 with only the null guard applied, the image XObject I get is
pixel-identical to the one in your [^PDFBOX-6235-cmyk.pdf] -- same stream
length (3851), same samples:
{noformat}
(10,10) (100,100) (100,190)
your PDF #001A2E #00642D #222600
my null-guard #001A2E #00642D #222600
{noformat}
So we are looking at exactly the same output.
h3. Cause
The stream itself is fine. What is missing is the {{/Decode}} array.
The ImageIO JPEG writer marks 4-component output with an Adobe APP14 segment
(the {{<app14Adobe transform="2"/>}} in your metadata dump), which means the
samples are stored inverted. {{createFromByteArray()}} already accounts for
this:
{code:java}
if (colorSpace instanceof PDDeviceCMYK)
{
COSArray decode = new COSArray();
... 1 0 1 0 1 0 1 0 ...
pdImage.setDecode(decode);
}
{code}
{{createJPEG()}} -- the path behind {{createFromImage()}} -- does not. It sets
DeviceCMYK from {{getColorSpaceFromAWT()}} and stops there, so the inverted
samples are interpreted as if they were direct. Hence the inversion you saw.
The asymmetry between the two paths is present in 2.0.x, 3.0.x and trunk alike.
h3. Result with the /Decode array added
Rendered output compared against {{createFromByteArray()}} on the same source
(mean absolute difference per channel, 0-255):
{noformat}
PDFBOX-6235-cmyk.jpg jpegcmyk.jpg (test resource)
null guard only mean 76.81 max 237 mean 143.82 max 255
null guard + /Decode mean 0.58 max 6 mean 5.79 max 255
{noformat}
The residual is JPEG re-encoding loss at the default quality. Visually:
[^PDFBOX-6235-decode-comparison.png]
h3. Patch
Attached as [^PDFBOX-6235.patch] (against trunk; applies to 2.0.x and 3.0.x
with offsets). No new imports are needed -- {{COSArray}}, {{COSInteger}},
{{PDColorSpace}} and {{PDDeviceCMYK}} are already imported in the file.
{code:java}
PDColorSpace colorSpace = getColorSpaceFromAWT(awtColorImage);
PDImageXObject pdImage = new PDImageXObject(document,
encodedByteStream, COSName.DCT_DECODE,
awtColorImage.getWidth(), awtColorImage.getHeight(), 8,
colorSpace);
if (colorSpace instanceof PDDeviceCMYK)
{
// the ImageIO JPEG writer marks 4-component output with an Adobe
APP14
// segment, so the samples are stored inverted, just like the input
handled
// in createFromByteArray(). PDFBOX-6235
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);
}
{code}
plus the null guard on the app0JFIF node as described in the issue.
h3. Test
No new test resource is needed -- the existing {{jpegcmyk.jpg}} works. This
fails on the current code with the NPE, fails with the null guard alone on both
the {{/Decode}} and the color assertion, and passes with the patch:
{code:java}
/**
* Tests JPEGFactory#createFromImage(PDDocument document, BufferedImage
image) with a
* 4-component (CMYK) image, as returned by ImageIO.read() for a CMYK JPEG.
PDFBOX-6235
*/
@Test
void testCreateFromImageCMYK() throws IOException
{
BufferedImage image;
try (InputStream is =
JPEGFactoryTest.class.getResourceAsStream("jpegcmyk.jpg"))
{
image = ImageIO.read(is);
}
// an ImageIO plugin that converts CMYK to RGB while reading (e.g.
TwelveMonkeys)
// would make this test pointless
assumeTrue(image.getColorModel().getColorSpace().getType() ==
ColorSpace.TYPE_CMYK);
BufferedImage reference;
try (PDDocument document = new PDDocument();
InputStream is =
JPEGFactoryTest.class.getResourceAsStream("jpegcmyk.jpg"))
{
reference = JPEGFactory.createFromStream(document, is).getImage();
}
try (PDDocument document = new PDDocument())
{
PDImageXObject ximage = JPEGFactory.createFromImage(document,
image);
validate(ximage, 8, 343, 287, "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());
// and the result must look like the same file embedded as-is
assertTrue(meanAbsDiff(reference, ximage.getImage()) < 10);
doWritePDF(document, ximage, TESTRESULTSDIR, "jpegcmykimage.pdf");
}
}
{code}
This needs three additions to the test file's imports
({{java.awt.color.ColorSpace}},
{{org.junit.jupiter.api.Assumptions.assumeTrue}},
{{java.awt.image.BufferedImage}} is already there) plus a small {{meanAbsDiff}}
helper -- or that last assertion can be dropped in favour of a few sampled
pixels if you prefer. Happy to shape it whichever way you want.
h3. No regression for other color spaces
TYPE_INT_RGB, TYPE_BYTE_GRAY and TYPE_INT_ARGB inputs produce byte-identical
encoded streams before and after the patch (SHA-256 of the raw stream), keep
their DeviceRGB/DeviceGray color space and still have no /Decode entry. The DPI
metadata continues to be written for them, since those do have an app0JFIF node.
Environment: JDK 25 (Corretto 25.0.1), PDFBox 3.0.7, no TwelveMonkeys on the
classpath.
> JPEGFactory.createFromImage() throws NullPointerException for 4-component
> (CMYK) BufferedImages
> -----------------------------------------------------------------------------------------------
>
> Key: PDFBOX-6235
> URL: https://issues.apache.org/jira/browse/PDFBOX-6235
> Project: PDFBox
> Issue Type: Bug
> Components: PDModel
> Affects Versions: 2.0.36, 3.0.1 PDFBox, 3.0.7 PDFBox
> Environment: PDFBox 3.0.1
> Java: Amazon Corretto 21.0.8+9-LTS (aarch64)
> OS: macOS (Apple Silicon)
> Reporter: Seungmin Lee
> Priority: Minor
> Labels: CMYK
> Fix For: 2.0.38, 3.0.9 PDFBox, 4.0.0
>
> Attachments: PDFBOX-6235-cmyk.jpg, PDFBOX-6235-cmyk.pdf,
> PDFBOX-6235-decode-comparison.png, PDFBOX-6235.patch
>
>
> JPEGFactory.createFromImage() fails with a NullPointerException for any
> BufferedImage backed by a 4-component CMYK color space -- for example the
> result of ImageIO.read() on a CMYK JPEG, which is common for print-ready
> assets.
> Reproduced on 2.0.36, 3.0.1 and 3.0.7 (same failure, only the line number
> differs: 396 / 372 / 376). The same unguarded dereference is still present on
> trunk.
> h3. Root cause
> encodeImageToJPEGStream() dereferences the app0JFIF metadata node without a
> null check:
> {code:java}
> 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); // <-- NPE
> jfif.setAttribute("Ydensity", dpiString);
> jfif.setAttribute("resUnits", "1");
> {code}
> The JFIF APP0 segment is only defined for 1-component (grayscale) and
> 3-component (YCbCr) JPEGs. When the ImageIO JPEG writer encodes a 4-component
> image it emits an Adobe APP14 marker instead, so the default metadata tree
> contains no "app0JFIF" node and item(0) returns null.
> Note that the NPE comes from the metadata of the *re-encoded output*, not
> from the input file -- a CMYK input JPEG that does carry a JFIF APP0 marker
> fails just the same.
> This appears to be an oversight rather than intentionally unsupported input:
> getColorSpaceFromAWT() explicitly handles ColorSpace.TYPE_CMYK, so the
> factory otherwise looks intended to accept 4-component images.
> h3. Steps to reproduce
> 1. Create a 4-component CMYK JPEG (no attachment needed):
> {noformat}
> magick -size 200x200 gradient:red-blue -colorspace CMYK cmyk.jpg
> {noformat}
> Verify with {{file cmyk.jpg}} -> "JPEG image data, baseline, precision 8,
> 200x200, components 4"
> 2. Run:
> {code:java}
> BufferedImage src = ImageIO.read(new File("cmyk.jpg"));
> // src.getType() == TYPE_CUSTOM (0)
> // src.getColorModel().getNumComponents() == 4
> // src.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_CMYK (9)
> try (PDDocument doc = new PDDocument()) {
> JPEGFactory.createFromImage(doc, src);
> }
> {code}
> h3. Actual result
> {noformat}
> java.lang.NullPointerException: Cannot invoke
> "org.w3c.dom.Element.setAttribute(String, String)" because "jfif" is null
> at
> org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory.encodeImageToJPEGStream(JPEGFactory.java:376)
> at
> org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory.createJPEG(JPEGFactory.java:312)
> at
> org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory.createFromImage(JPEGFactory.java:278)
> at
> org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory.createFromImage(JPEGFactory.java:255)
> at
> org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory.createFromImage(JPEGFactory.java:233)
> {noformat}
> (stack trace from 3.0.7)
> h3. Expected result
> Either a PDImageXObject is created with a DeviceCMYK color space, or a
> descriptive exception is thrown stating that 4-component images are not
> supported.
> h3. Suggested fix
> Guard the dereference. The DPI metadata simply cannot be expressed in a
> non-JFIF stream, so skipping those three attributes when jfif is null seems
> sufficient:
> {code:java}
> Element jfif = (Element) tree.getElementsByTagName("app0JFIF").item(0);
> if (jfif != null)
> {
> String dpiString = Integer.toString(dpi);
> jfif.setAttribute("Xdensity", dpiString);
> jfif.setAttribute("Ydensity", dpiString);
> jfif.setAttribute("resUnits", "1"); // 1 = dots/inch
> }
> {code}
> h3. Workaround
> When the image does not need resampling, embedding the original bytes with
> JPEGFactory.createFromByteArray() avoids the re-encode entirely and also
> preserves the DeviceCMYK color space. This is not an option when the image
> must be resized.
> Possibly related (all older, different symptoms): PDFBOX-2057, PDFBOX-2128,
> PDFBOX-3823.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]