B-Teck!

お仕事からゲームまで幅広く

【Kotlin】画像をリサイズしたり切り抜いたりする

仕事で、URLから画像を取得していい感じにリサイズする要件があったため調べました。
備忘のためメモを書いておきます。

URLから画像の取得

今回はURLから画像を取得して編集することが要件だったので、まずはURLから画像を取得する処理です。
後々の取り回しのためにByteArrayで取得しておきます。

import java.net.HttpURLConnection
import java.net.URL

fun getImageBytes(url: String): ByteArray {
    lateinit var conn: HttpURLConnection
    return try {
        conn = URL(url).openConnection() as HttpURLConnection
        conn.requestMethod = "GET"
        conn.inputStream.readBytes()
    } finally {
        conn.disconnect()
    }
}

画像の加工

BufferedImageへ変換

取得した画像を編集するために、BufferedImageに変換します。

// 画像読み込み
javax.imageio.ImageIO.read(imageBytes.inputStream())

画像のリサイズ

縦横の幅を指定して新たなBufferedImageとして描画し直すことでリサイズできます。

import java.awt.image.BufferedImage

fun resize(image: BufferedImage, width: Int, height: Int): BufferedImage =
    BufferedImage(width, height, BufferedImage.TYPE_INT_RGB).also {
        val g = it.createGraphics()
        g.drawImage(image, 0, 0, width, height, null)
        g.dispose()
    }

画像の切り抜き

BufferdImage.getSubimage() を使うと、画像の一部を切り出すことができます。
x, yで切り抜きの始点を指定し、そこからwidthとheightで指定した分の画像を切り抜いた新しいBufferdImageを生成します。
このとき、指定した座標などが画像の範囲外になるとエラーになるので注意してください。

image.getSubimage(x, y, width, height)

画像の保存

BufferdImageはOutputStreamやFileのようなオブジェクトに書き込むことで保存できます。

// ByteArrayOutputStreamに書き込んでByteArrayに変換する場合
ByteArrayOutputStream().let {
        ImageIO.write(croppedImage, "PNG", it)
        it.toByteArray()
    }

// Fileオブジェクトに直接書き込む場合
ImageIO.write(croppedImage, "PNG", File("ファイル名.png")

(おまけ)画像をいい感じにリサイズして切り抜く

今回求められていた要件は、与えられた画像を指定のサイズに余白なく収まるようにリサイズした上で、中心を基準に画像を切り抜くというものでした。
文字だけだと伝わりづらいと思うので例を示すとこんな感じです。
f:id:beatdjam:20210321050258p:plain
f:id:beatdjam:20210321050313p:plain
f:id:beatdjam:20210321050328p:plain

この処理を実装するためには、

  • 元画像と描画先の比率からリサイズの倍率を求めてリサイズする
  • 描画領域に応じて切る方向を決める
  • 中心から適切に画像を切り抜く

という手順が必要でした。

欲しいサイズに合わせてリサイズ

縦、横それぞれの元画像、編集後の比率を求めて、差の大きい方を基準の倍率としてリサイズします。

import java.awt.image.BufferedImage
import kotlin.math.ceil

fun resize(image: BufferedImage, width: Int, height: Int): BufferedImage {
    // 横幅・縦幅の比率を計算して、大きい方を基準にリサイズする
    val widthScale = width.toDouble() / image.width.toDouble()
    val heightScale = height.toDouble() / image.height.toDouble()
    val scale = if (widthScale > heightScale) widthScale else heightScale

    val resizeWidth = ceil(image.width * scale).toInt()
    val resizeHeight = ceil(image.height * scale).toInt()
    return BufferedImage(resizeWidth, resizeHeight, BufferedImage.TYPE_INT_RGB).also {
        val g = it.createGraphics()
        g.drawImage(image, 0, 0, resizeWidth, resizeHeight, null)
        g.dispose()
    }
}

中心から画像を切り抜く

元画像と出力結果の向きから画像の切り取り方向を決定し、切り抜きます。
切り抜く座標の原点は画像の中心から出力結果の半分を引くことで求めることができます。

import java.awt.image.BufferedImage

fun crop(image: BufferedImage, width: Int, height: Int): BufferedImage? {
    // 元画像が横長か縦長かを判定
    val isHorizontalImage = image.width > image.height
    // 出力結果が縦長か横長かを判定
    val isHorizontalView = width > height
    return when {
        // 横長の画像を縦長のViewに表示する場合
        isHorizontalImage && !isHorizontalView -> {
            val x = (image.width.toDouble() / 2 - width.toDouble() / 2).toInt()
            image.getSubimage(x, 0, width, height)
        }
        // 縦長の画像を縦長のViewに表示する場合
        else -> {
            val y = (image.height.toDouble() / 2 - height.toDouble() / 2).toInt()
            image.getSubimage(0, y, width, height)
        }
    }
}

サンプル実装

おまけで記載している処理については下記のファイルに一通りが記載されています。
手元で実行すれば、冒頭であげた画像のような出力結果が得られます。
sandbox/20210320.kt at 686f532c6dbddda43262df1673e015870a5bf5ad · beatdjam/sandbox · GitHub

おわりに

Java標準のライブラリを使って、思いの外自由に画像の編集が行えることがわかりました。
ググって出てくるのはほとんどJavaの実装だったので、Kotlinで同じようなことをしてみたい方の役に立てばいいなと思っています。
おまけの部分の実装は、思いついた実装でどうにかしたという感じなので、より良い方法をご存知の方はコメントやTwitterなどで教えて下さい。