javajpegjavax.imageioaffinetransformbicubic

Java AWT/ImageIO: Bilinear and Bicubic scaling of a JPEG image result in completely black output


Nearest neighbor scaling works: The entire picture stays intact when I use TYPE_NEAREST_NEIGHBOR.

Even though it is Scala code, all used libraries are standard Java libraries.

Functions:

def getBufferedImage(imageFile: java.io.File): BufferedImage = {
    ImageIO.read(imageFile)
}

def scaleImage(image: BufferedImage, minSize: Double): BufferedImage = {
    val before: BufferedImage = image
    val w = before.getWidth()
    val h = before.getHeight()
    val affit = new AffineTransform()
    var scale = 1.0
    if(h < w) {
      if(h > 0) {
        scale = minSize / h
      }
    } else {
      if(w > 0) {
        scale = minSize / w
      }
    }
    affit.scale(scale, scale)
    val affitop = new AffineTransformOp(affit, AffineTransformOp.TYPE_BICUBIC)
    affitop.filter(before, null)
}

def getImageJpegByteArray(image: BufferedImage): Array[Byte] = {
    val baos = new java.io.ByteArrayOutputStream()
    val mcios = new MemoryCacheImageOutputStream(baos)
    ImageIO.write(image, "jpeg", mcios)
    mcios.close()
    baos.toByteArray
}

Calling code snippet:

val img = getBufferedImage(imageFile)
val scaledImg = scaleImage(img, 512)
val result = getImageJpegByteArray(scaledImg)
// result is written to SQLite database

result is written to an SQLite database. If I download it from the database and save it as JPEG file, the resulting JPEG is

Consequently, I accuse AffineTransformOp of being buggy... How can I solve this problem?

File magic number of result is always ff d8 ff as expected for JPEG.

Details

Java version: Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_71

Operating System: Apple, OS X 10.9.5

Test image: http://www.photos-public-domain.com/wp-content/uploads/2012/05/thundercloud-plum-blossoms.jpg


Solution

  • I was able to reproduce your issue on Java 1.7.0_71 on OS X 10.10.4 (I rewrote your code in Java, I can post the full code if you are interested).

    In any case, the problem is not that AffineTransformOp is buggy in itself. In my test program I displayed the image using a minimal Swing JFrame and the scaled image looked all good there. This is likely why most people in the comments did not understand the problem.

    Part of the issue is that the BufferedImage returned by AffineTransformOp when you don't provide a destination to the filter method (the second parameter, null in your case), it will create one for you. This image will get type BufferedImage.TYPE_INT_ARGB. Here is the relevant code from AffineTransformOp.createCompatibleDestImage() (lines 456-468, I kept the formatting, to make it easier to spot):

    ColorModel cm = src.getColorModel();
    if (interpolationType != TYPE_NEAREST_NEIGHBOR &&
        (cm instanceof IndexColorModel ||
         cm.getTransparency() == Transparency.OPAQUE)
    {
        image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
    }
    else {
        image = new BufferedImage(cm,
                  src.getRaster().createCompatibleWritableRaster(w,h),
                  cm.isAlphaPremultiplied(), null);
    }
    

    Notice the special case for TYPE_NEAREST_NEIGHBOR, which explains why you'll get different behavior when using nearest neighbor algorithm. Normally this is all good, however (as I said, the image displays just fine in a Swing component).

    The problem arises when you try to store this image as a JPEG. During the years, there's been a lot of confusion and issues related to the ImageIO JPEG plugin and whether it will allow you to write images with alpha channel (like your TYPE_INT_ARGB image). It does allow that. But, most often ARGB JPEGs will get misinterpreted as CMYK JPEGs (as they are 4 channels, and storing ARGB data in JPEG is very exotic) and will be displayed in all funky colors. In your case though, it seems to be all black...

    So, there are two possible solutions:

    Here is your image, scaled by the program, using JPEG format and the TYPE_3BYTE_BGR:

    Scaled image

    I'm sure you can rewrite my Java code back to Scala. :-)