[ 
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]

Reply via email to