2012年8月11日土曜日

java : 今更ながらImageIO.readの不具合を踏んだ

今更ながらImageIO.readの不具合を踏みました。 「jpgファイルのフォーマットによっては正常に読み込めない場合がある」というアレです。 ずいぶん前からあるネタのようですが、ぜんぜん知らなかったのでビックリ。 直らないもんなんですかね?

きっかけは、サムネイルを作るコードを書こうとしたことでした。 まずは検索でサラッと

  • 縮小画像を作るときはGraphics2D.drawImage(x,y,width,height,ImageObserver)を使う。
  • そのときの画質はRenderingHintsで調整できる。

というのを確認。

で、RenderingHintsの設定でどの程度違いが出るのかを確認したくて簡単なテストコードを書きました。 たいして時間もかからずに完成。 「楽でいいなぁ」などと思いつつサンプル画像を変えて試してみたら出力画像が真っ赤 → ナンダコレハという流れです。

さらに検索して、この不具合の対処法を発見。 「jpgファイルのときだけToolkit.getDefaultToolkit().createImage(ソース)を使いなさい」という事らしいですね。 こんなコードになりました。

// ThumbnailCreator.java
package createthumbnailtest;

import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;

public class ThumbnailCreator
{
    private static Component _dummyComponent;

    private int _size;
    private Map<RenderingHints.Key, Object> _renderingHints;

    public ThumbnailCreator(int size)
    {
        _size = size;
    }

    public void setRenderingHints1()
    {
        _renderingHints = new HashMap<>(32);
        _renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    }

    public void setRenderingHints2()
    {
        _renderingHints = new HashMap<>(32);
        _renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        _renderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        _renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        _renderingHints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        _renderingHints.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
        _renderingHints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    }

    public void setRenderingHints3()
    {
        _renderingHints = new HashMap<>(32);
        _renderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        _renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        _renderingHints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        _renderingHints.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
        _renderingHints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    }

    public void setRenderingHints4()
    {
        _renderingHints = new HashMap<>(32);
        _renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
    }

    public BufferedImage create(byte[] src, boolean isJpg) throws IOException
    {
        Image img = null;
        if(isJpg)
        {
            img = Toolkit.getDefaultToolkit().createImage(src);
            if(_dummyComponent == null)
            {
                _dummyComponent = new Component(){};
            }
            MediaTracker mediaTracker = new MediaTracker(_dummyComponent);
            mediaTracker.addImage(img, 0);
            try
            {
                mediaTracker.waitForID(0);
            }
            catch (InterruptedException exc)
            {
                // TODO ログをはく
                return null;
            }
        }
        else
        {
            try(ByteArrayInputStream in = new ByteArrayInputStream(src) )
            {
                img = ImageIO.read(in);
            }
        }

        if(img == null)
            return null;

        int width = img.getWidth(null);
        int height = img.getHeight(null);
        if(width == -1 || height == -1)
            return null;

        if(_size < width || _size < height)
        {
            if(height <= width)
            {
                height = _size * height / width;
                width = _size;
            }
            else
            {
                width = _size * width / height;
                height = _size;
            }
        }

        int imageType = (isJpg ? BufferedImage.TYPE_INT_RGB : ( (BufferedImage)img).getType() );
        BufferedImage res = new BufferedImage(width, height, imageType);
        Graphics2D g = res.createGraphics();
        g.setRenderingHints(_renderingHints);
        g.drawImage(img, 0, 0, width, height, null);

        return res;
    }
}

createメソッドの引数がbyte配列になってるのは、これを使うコードの都合です。 「TODO ログをはく」とか変なことを書いてるのもそっちにあわせた都合。

で、ThumbnailCreatorを動かしてみるテストコードは、

// CreateThumbnailTest.java
package createthumbnailtest;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.imageio.ImageIO;

public class CreateThumbnailTest
{
    private static final String EXTENSION = "jpg";
    private static final String FILE_PATH = "適当なパス\\test";

    public static void main(String[] args)
    {
        ThumbnailCreator thumbnailCreator = new ThumbnailCreator(180);

        try
        {
            File srcFile = new File(FILE_PATH + '.' + EXTENSION);
            int srcSize = (int)srcFile.length(); // 簡単なテストなのでlongにはしない
            FileInputStream in = new FileInputStream(srcFile);
            byte[] srcBuff = new byte[srcSize];

            in.read(srcBuff);

            thumbnailCreator.setRenderingHints1();
            BufferedImage thumbnail1 = thumbnailCreator.create(srcBuff, EXTENSION.equals("jpg") );
            writeThumbnail(thumbnail1, 1);

            thumbnailCreator.setRenderingHints2();
            BufferedImage thumbnail2 = thumbnailCreator.create(srcBuff, EXTENSION.equals("jpg") );
            writeThumbnail(thumbnail2, 2);

            thumbnailCreator.setRenderingHints3();
            BufferedImage thumbnail3 = thumbnailCreator.create(srcBuff, EXTENSION.equals("jpg") );
            writeThumbnail(thumbnail3, 3);

            thumbnailCreator.setRenderingHints4();
            BufferedImage thumbnail4 = thumbnailCreator.create(srcBuff, EXTENSION.equals("jpg") );
            writeThumbnail(thumbnail4, 4);
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
        }
    }

    private static void writeThumbnail(BufferedImage thumbnailImg, int testNum) throws IOException, NoSuchAlgorithmException
    {
        byte[] thumbnailBuff = bufferedImageToBytes(thumbnailImg);

        File outFile = new File(FILE_PATH + testNum + '.' + EXTENSION);
        try(FileOutputStream out = new FileOutputStream(outFile) )
        {
            out.write(thumbnailBuff);
            out.flush();
        }

        System.out.println("[" + testNum + "] ... size = " + thumbnailBuff.length);
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(thumbnailBuff);
        System.out.println("[" + testNum + "] ... md5 = " + md.digest() );
    }

    public static byte[] bufferedImageToBytes(BufferedImage src)
    {
        try(ByteArrayOutputStream out = new ByteArrayOutputStream() )
        {
            ImageIO.write(src, EXTENSION, out);
            return out.toByteArray();
        }
        catch(IOException exc)
        {
            // TODO ログをはく
            return null;
        }
    }
}

なんか、1度「サムネイル作るコードって簡単なんだなぁ」と思ってしまったせいか、このコードが雑然として見えてしまいます。 仕方が無いのかな?

で、肝心の「RenderingHintsの違いは?」というと、私の眼力では「どれでもいいんじゃない?」となりました。 まぁ、1番たくさん設定を書いたsetRenderingHints2がいいのかなぁ...?

他の人が画質についてアレコレ書けているのと比べると、自分の眼力にはガッカリしておいた方がいいんでしょうか? ImageIO.readにガッカリするより自分の眼力にガッカリする方に重きを置けと。