Saving JPEGs: ImageIO gotchas

While the first of the Java 2D classes that I used in my thumbnail-generation code was AffineTransformOp, in order to actually see the results, I needed to save the image. Fortunately, Java includes a library called ImageIO that can save images in various formats.

The code to write out a JPEG is pretty straightforward, though if you want to set a specific quality, you can't use the one-line version. But when I opened the resulting file in Preview (I use a Mac), it was completely black!

I've had experiences with weird JPEG variants before, and I figured that maybe Preview was lame and that a better program would be able to display the image. So I opened it in Photoshop. This time, I was able to see the image, but all the colors were wrong. According to Photoshop, it was a CMYK image. But the original image was RGB. How could Java 2D be loading an RGB image, scaling it, then producing a CMYK JPEG? And anyway, the file obviously wasn't really a CMYK image: the colors were all wrong. It was a regular RGB image that Photoshop was reading as CMYK.

I looked for information about CMYK JPEGs, so I could maybe figure out what ImageIO was doing wrong, but I couldn't find much real information about them. Instead I mostly found posts from other people having problems with them.

I wound up getting some code that I knew was able to generate a BufferedImage and save it out as an RGB JPEG successfully, and I started gradually converting it to my code in order to determine at which point it started creating the CMYK JPEG instead. That point turned out to be immediately after I added AffineTransformOp.

The only obvious difference that I found between the BufferedImage that ImageIO saved correctly and the one that it saved incorrectly as a CMYK JPEG was that the former had a type of TYPE_INT_RGB and the latter, which came out of AffineTransformOp, had a type of TYPE_INT_ARGB. In other words, the output of AffineTransformOp contained an alpha channel. An ARGB image contains four channels. A CMYK image contains four channels. A-ha!

I didn't investigate CMYK JPEGs much further after that, but my theory is that Photoshop interprets any four-channel JPEG as a CMYK JPEG. Or maybe the file that ImageIO writes doesn't contain a color profile, and Photoshop defaults to CMYK because it has four channels. In either case, Preview cannot display the four-channel image.

The solution was to create an intermediate BufferedImage of TYPE_INT_RGB, use Graphics2D.drawImage to copy the AffineTransformOp output to the intermediate image, then save it out.

But why was AffineTransformOp creating an ARGB image in the first place? After all, I passed in an RGB image. Shouldn't the output match the input? I traced into the source code to see what was going on, and I discovered that even if I passed in an RGB image as the destination, AffineTransformOp would ignore it and create an ARGB image. Why? It has to do with AffineTransformOp's ability to perform any kind of affine transform, which might involve rotating or skewing the image. In these cases, the transformed image may not occupy the entire area of the output raster. For example, if AffineTransformOp is being used to rotate the image by 45 degrees, then the output raster will still be rectangular--because all rasters are rectangular--but will contain a diamond-shaped image in the middle. The pixels of the raster that fall outside the image must be transparent, so the BufferedImage that AffineTransformOp uses for its output must have an alpha channel. If AffineTransformOp tried to use a regular TYPE_INT_RGB image, then those pixels outside the diamond-shaped image would come out black, probably not what the caller wants.

Once again, it seems like Java 2D does the right thing, even if its rationale is sometimes hard to figure out.

Comments

Popular posts from this blog

UUIDs as primary keys

Scala and Kotlin