JDK-8313303 : Invalid behaviour of JPEGImageWriter.getDefaultImageMetadata
  • Type: Bug
  • Component: client-libs
  • Sub-Component: javax.imageio
  • Affected Version: 8,11,17,20,21
  • Priority: P3
  • Status: Open
  • Resolution: Unresolved
  • OS: generic
  • CPU: generic
  • Submitted: 2023-07-21
  • Updated: 2023-10-20
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdUnresolved
Related Reports
Relates :  
Description
A DESCRIPTION OF THE PROBLEM :
Usually ImageIO library creates JPEG in YUV color space. But I work with TIFF files, where RGB-encoded JPEG tiles are popular enough. I've found a way to create such JPEG, using documented ImageIO features.

In doing so, I've found some obvious bugs. Please see the attached source code (or download it here: <LINK> ). Please compile it and run with some source image file as the 1st argument (not important, you may use a screenshot) and some JPEG result file as the 2nd argument.

It works correctly, and the stored JPEG has RGB space - for example, you can check this fact with help of JHove utility (<LINK>).

Please change the constant ENFORCE_BUG_1 to true. You will see:
Exception in thread "main" java.lang.InternalError: Default image metadata is inconsistent
	at java.desktop/com.sun.imageio.plugins.jpeg.JPEGMetadata.<init>(JPEGMetadata.java:622)
	at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageWriter.getDefaultImageMetadata(JPEGImageWriter.java:238)
	at net.algart.matrices.io.formats.tests.AWTWriteJpegBug.main(AWTWriteJpegBug.java:98)

It is thrown by JPEGMetadata constructor. But InternalError should not occur at all in a correct code, when I used only documented public methods! If some arguments are inconsistent, you should throw NullPointerException or IllegalArgumentException.

Then, please set ENFORCE_BUG_1 back to false and set ENFORCE_BUG_2 = true. In this case, the program works without exceptions, but... results are wrong: saved JPEG contains YCbCr color space, not RGB. You can check this by JHove or just look into the source code of JPEGMetadata constructor
JPEGMetadata(ImageTypeSpecifier imageType,
                 ImageWriteParam param,
                 JPEGImageWriter writer)
Due to some reasons, it ignores param.getDestinationType() ("destType = null"), when the first argument is not null. It seems illogical: we MUST specify null imageType, when we really NEEDS it to inform about the desired color space RGB.

Then, just look at my last comment "!!!!!! BUG #3!". If we pass 2 null arguments to writer.getDefaultImageMetadata, it also leads to InternalError, that is obviously incorrect behaviour for public method: it should throw NullPointerException or something like this.

The last question: why is it so difficult to write JPEG with color space, other than YCbCr?? And why we have no documented way to know, which color space is really encoded inside a given JPEG file, without using debugger or external tools like JHove?



---------- BEGIN SOURCE ----------
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;

public class AWTWriteJpegBug {
    public static final boolean ENFORCE_BUG_1 = false;
    public static final boolean ENFORCE_BUG_2 = false;

    public static final boolean NEED_JCS_RGB = true;

    public static ImageWriter getJPEGWriter() throws IIOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpeg");
        if (!writers.hasNext()) {
            throw new IIOException("Cannot write JPEG");
        }
        return writers.next();
    }

    public static ImageWriteParam getJPEGWriteParam(ImageWriter writer, ImageTypeSpecifier imageTypeSpecifier) {
        ImageWriteParam writeParam = writer.getDefaultWriteParam();
        writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        writeParam.setCompressionType("JPEG");
        if (ENFORCE_BUG_1) {
            // !!!!!! BUG #1!
            // If we do not call setDestinationType below or pass null as imageTypeSpecifier,
            // then the method
            // writer.getDefaultImageMetadata(null, writeParam)
            // in main() method will throw InternalError!
            // !!!!!!
            return writeParam;
        }
        if (imageTypeSpecifier != null) {
            writeParam.setDestinationType(imageTypeSpecifier);
            // - Important! It informs getDefaultImageMetadata to add Adobe and SOF markers,
            // that is detected by JPEGImageWriter and leads to correct outCsType = JPEG.JCS_RGB
        }
        return writeParam;
    }

    public static void main(String[] args) throws IOException {
        if (args.length < 2) {
            System.out.println("Usage:");
            System.out.println("    " + AWTWriteJpegBug.class.getName()
                    + " some_image.jpeg result.jpeg");
            return;
        }

        final File srcFile = new File(args[0]);
        final File resultFile = new File(args[1]);

        System.out.printf("Opening %s...%n", srcFile);
        BufferedImage bi = ImageIO.read(srcFile);
        if (bi == null) {
            throw new IOException("Unknown format");
        }

        System.out.printf("Writing JPEG image into %s...%n", resultFile);
        resultFile.delete();
        ImageWriter writer = getJPEGWriter();
        System.out.printf("Registered writer is %s%n", writer.getClass());

        ImageOutputStream ios = ImageIO.createImageOutputStream(resultFile);
        writer.setOutput(ios);

        ImageTypeSpecifier imageTypeSpecifier = new ImageTypeSpecifier(bi);

        ImageWriteParam writeParam = getJPEGWriteParam(writer, NEED_JCS_RGB ? imageTypeSpecifier : null);
        IIOMetadata metadata;
        if (ENFORCE_BUG_2) {
            metadata = writer.getDefaultImageMetadata(imageTypeSpecifier, writeParam);
            // !!!!!! BUG #2!
            // If we shall pass imageTypeSpecifier instead of null as the 1st argument of getDefaultImageMetadata
            // (it looks  absolutely correct!),
            // then this method will IGNORE destination type, specified by writeParam.setDestinationType
            // inside getJPEGWriteParam, and we will make Y/Cb/Cr instead of desired RGB color space. Why??
            // !!!!!!
        } else {
            metadata = writer.getDefaultImageMetadata(NEED_JCS_RGB ? null : imageTypeSpecifier, writeParam);
        }


        // metadata = writer.getDefaultImageMetadata(null, null);
        // !!!!!! BUG #3!
        // If you will uncomment operator above, it will also lead to InternalError
        // instead of more suitable NullPointerException or something like this.
        // !!!!!!

        IIOImage iioImage = new IIOImage(bi, null, metadata);
        writer.write(null, iioImage, writeParam);
        ios.close();
    }
}

---------- END SOURCE ----------

FREQUENCY : always



Comments
Checked with attached testcase in Windows 10, same behavior observed in JDK 8, 11, 17, 20 and 21ea Moving to JDK Project for more evaluation.
28-07-2023