javajpegjavax.imageiocolor-profile

Safely discarding ICC profile information in Java Image I/O


I'm using Java Image I/O with Java 11 to read a JPEG image, scale it by drawing a scaled version, and then writing it. (I am using this technique because it produces good results and the JAI subsample average technique I tried left black borders on some sides of the image.)

Importantly I am discarding all metadata. (I separately write a tiny bit of metadata back to the final image later, but that is not relevant to this discussion.)

…
imageReader.setInput(imageInputStream, true, true); //ignore metadata
oldImage = imageReader.read(0, imageReadParam);
…
Image scaledImage = oldImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH);
int oldImageType = oldImage.getType();
int newImageType = oldImageType != BufferedImage.TYPE_CUSTOM ? oldImageType //use the existing image type if it isn't custom
    : (oldImage.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; //otherwise use RGB unless ARGB is needed for transparency
newImage = new BufferedImage(newWidth, newHeight, newImageType);
final Graphics2D graphics = newImage.createGraphics();
try {
  graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
  graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
  graphics.drawImage(scaledImage, null, null);
} finally {
  graphics.dispose();
}
scaledImage.flush();
…
imageWriter.setOutput(imageOutputStream);
IIOImage iioImage = new IIOImage(newImage, null, null); //write with no thumbnails and no metadata
imageWriter.write(null, iioImage, imageWriteParam);
…

Some of the input images have ICC profile metadata section. I worry whether discarding the ICC profile metadata will change how the resulting image will appear. I understand what image color profiles are conceptually, but I don't know how they work in files and how they interact with color spaces.

My question summarized: If I invariably discard ICC profile and other profile-related metadata sections when processing images using Java Image I/O, how can I ensure that my resulting image colors will be correct?

From a discussion with someone, I inferred that these ICC profiles may be for color spaces other than sRGB. (Is that true?) If so, does converting the image to sRGB with Java Image I/O take the ICC profile into account so that I can discard it? And if so, how do I know if I'm converting to sRGB or not?

In the code above I try to use the existing image type (because I thought it was a good idea not to change anything) unless the type is "custom", in which case I choose one of the simple RGB types.

int newImageType = oldImageType != BufferedImage.TYPE_CUSTOM ? oldImageType //use the existing image type if it isn't custom
    : (oldImage.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; //otherwise use RGB unless ARGB is needed for transparency

But if I use the old image type if possible, might that use a color space other than RGB, needing the ICC profile? And then would the image colors be incorrect if I discard the ICC profile? Should I somehow force Java Image I/O to convert to RGB?

Part of my doubt is undoubtedly due to my not understanding how color profiles work in files, so a reference to some simplified overview would also be appreciated if you know of a good one (that doesn't get lost down in the mud with byte-level details).


Solution

  • Short answer: If you discard the ICC color profile from the JPEG, there's no way to ensure the colors will be correct (unless you also fully control the reading of these files).

    However, the current code does not just "discard" ICC profiles, instead the images will always end up as plain RGB color space, using sRGB profile, and will be properly converted by Java2D along the way. As the sRGB profile will typically not be embedded, your images will display as intended using software that uses sRGB as default profile.


    Long answer:

    1. Passing true for the ignoreMetadata of ImageReader.setInput method, is simply a hint to the reader that it may ignore meta data. It does not enforce skipping meta data (and most likely, an ICC profile should not be considered metadata anyway). From the JavaDoc:

      The ignoreMetadata parameter, if set to true, allows the reader to disregard any metadata encountered during the read. [...] The reader may choose to disregard this setting and return metadata normally.

    2. When dealing with BufferedImages in Java, images will always use a color profile. Your oldImage should either have its original color profile from the JPEG, or be converted from this profile to sRGB. Exactly how this works (if at all) is entirely plugin dependent. But to the best of my understanding, the standard ImageIO JPEG plugin will read the embedded ICC profile, and convert the results to sRGB.

    3. Exactly what happens when you use Image.getScaledImage I'm not sure, as the old AWT Image API is not something I use. Most likely, the image was already using sRGB and will still use sRGB profile. It should not modify color.

    4. In any case, newImage will always be RGB in sRGB profile, as there is no profile information carried over to the BufferedImage constructor used. Non-RGB images will always be TYPE_CUSTOM, and thus converted to TYPE_INT_(A)RGB. Note that images with alpha channels will typically not be interpreted as intended by other JPEG software, and I believe support was removed from recent JDKs, so my advice is to always remove any alpha channel before writing as JPEG.

    5. Finally, writer.write (without any metadata) will, for the standard ImageIO JPEG plugin simply write the image as JPEG in JFIF format (if possible). It will only write an ICC profile if the image to be written is in a non-standard profile. For sRGB it will not write one (I'm assuming imageWriteParam are all defaults).

    PS: The original JFIF format did not specify a default color profile, but I believe later amendments (like IEC 61966-2-1) does specify sRGB as default. The Exif format allows specifying sRGB as the color profile without actually embedding it, by setting the Exif ColorSpace tag (tag 40961) to 1. The standard JPEG plugin does not write Exif files (without significant code on your part).

    You can verify most of the statements above by carefully studying the source code for the JDK.