Javaで画像の特定の色を透過させる

前回、アップロードされた画像の加工をJavaでやったところ、追加で「透過していない画像であれば、白背景を透過できないか」との要望が。

ちょっと試してみたところ、何とかなったのでメモ。

環境

OpenJDK 11

実装

前回同様、 java.awt.image のクラスを利用。

透過済みかの判定

BufferedImage#isAlphaPremultiplied で、アルファが事前に乗算されている*1かは判定できるが、そうでない場合の透過は判定できないので、画像のピクセルを総当たりしてアルファが設定されているか判定する。

boolean hasAlpha(BufferedImage image) {
    // アルファが乗算済みか判定
    if (image.isAlphaPremultiplied()) {
        return true;
    }

    // 乗算済みでない場合、各ピクセルをチェックし、アルファが設定されているか判定
    var imageWidth = image.getWidth();
    var imageHeight = image.getHeight();

    for (int x = 0; x < imageWidth; x++) {
        for (int y = 0; y < imageHeight; y++) {
            var alpha = new Color(image.getRGB(x, y), true).getAlpha();

            // アルファ未設定の場合255になる
            if (alpha != 255) {
                return true;
            }
        }
    }

    return false;
}

特定の色を透過

RGBImageFilter を使って、特定の色の場合に透過させることができる。

class TransparentFilter extends RGBImageFilter {

    private static final int RGB_BITS = 0xFFFFFF;

    private final int targetRgb;

    TransparentFilter(int targetRgb) {
        // RGBだけ抽出
        this.targetRgb = targetRgb & RGB_BITS;
        canFilterIndexColorModel = true;
    }

    TransparentFilter(Color color) {
        this(color.getRGB());
    }

    @Override
    public int filterRGB(int x, int y, int rgb) {
        // ビット演算でRGBを抽出し、対象と一致した場合透過させる
        // アルファが0になればいいので、0を返す
        return (rgb & RGB_BITS) == targetRgb ? 0 : rgb;
    }

}

のようなサブクラスを作成。

以下のような処理で、色を指定して透過できた。

void transparent(File imageFile, Color color) throws IOException {
    var originalImage = ImageIO.read(imageFile);

    // 前述のメソッドで透過済みか判定し、透過済みなら何もせず返す
    if (hasAlpha(originalImage)) {
        return;
    }

    var imageFilter = new TransparentFilter(color);
    var imageProducer = new FilteredImageSource(
            originalImage.getSource(), imageFilter
    );
    var transparentImage = Toolkit
                .getDefaultToolkit()
                .createImage(imageProducer);

    // 元画像と同じ大きさの透過画像を作成
    var newImage = new BufferedImage(
            originalImage.getWidth(),
            originalImage.getHeight(),
            BufferedImage.TYPE_INT_ARGB
    );
    var graphics = newImage.createGraphics();
    graphics.drawImage(transparentImage, 0, 0, null);

    // ファイルを上書き
    ImageIO.write(newImage, "png", imageFile);
    return;
}

実行サンプル

表計算ソフトで適当な画像を用意。

f:id:hepokon365:20211115000117p:plain
適当にもほどがある画像

Color.WHITEColor.BLACK を指定してそれぞれ実行した結果が以下。

f:id:hepokon365:20211115000307p:plainf:id:hepokon365:20211115000356p:plain
左がWHITE、右がBLACK指定

振り返り

まさか2021年にもなって、Javaで画像処理をガッツリやるとは思わなかった。

もろもろの画像編集ツールとかを使うと簡単にできることだが、情報が少ない&古いので、なかなか面倒だった。

ただ、 java.awt.image は枯れ切ってるので、10年以上前の情報もそのまま使えたりするのはちょっと面白かった。

参考

TYPE_INT_ARGBTYPE_INT_ARGB_PRE の違いについて

stackoverflow.com

*1:RGBと別にAlphaを持って画像を描画する際に乗算する方法と、あらかじめRGBにAlphaを乗算しておく方法があり、後者を指す模様。それぞれ、TYPE_INT_ARGB と TYPE_INT_ARGB_PRE に相当する。