SubwayTooter-Android-App/app/src/main/java/jp/juggler/util/BitmapUtils.kt

296 lines
8.8 KiB
Kotlin

package jp.juggler.util
//import it.sephiroth.android.library.exif2.ExifInterface
import android.content.Context
import android.graphics.*
import android.net.Uri
import androidx.annotation.StringRes
import androidx.exifinterface.media.ExifInterface
import java.io.FileNotFoundException
import java.io.InputStream
import kotlin.math.max
import kotlin.math.sqrt
private val log = LogCategory("BitmapUtils")
fun InputStream.imageOrientation(): Int? =
try {
ExifInterface(this)
// .readExif(
// this@imageOrientation,
// ExifInterface.Options.OPTION_IFD_0
// or ExifInterface.Options.OPTION_IFD_1
// or ExifInterface.Options.OPTION_IFD_EXIF
// )
.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)
.takeIf { it >= 0 }
} catch (ex: Throwable) {
log.w(ex, "imageOrientation: exif parse failed.")
null
}
// 回転情報の値に合わせて、wとhを入れ替える
fun rotateSize(orientation: Int?, w: Float, h: Float): PointF =
when (orientation) {
5, 6, 7, 8 -> PointF(h, w)
else -> PointF(w, h)
}
// 回転情報を解決するようにmatrixに回転を加える
fun Matrix.resolveOrientation(orientation: Int?): Matrix {
when (orientation) {
2 -> postScale(1f, -1f)
3 -> postRotate(180f)
4 -> postScale(-1f, 1f)
5 -> {
postScale(1f, -1f)
postRotate(-90f)
}
6 -> postRotate(90f)
7 -> {
postScale(1f, -1f)
postRotate(90f)
}
8 -> postRotate(-90f)
}
return this
}
enum class ResizeType {
// リサイズなし
None,
// 長辺がsize以下になるようリサイズ
LongSide,
// 平方ピクセルが size*size 以下になるようリサイズ
SquarePixel,
}
class ResizeConfig(
val type: ResizeType,
val size: Int,
@StringRes val extraStringId: Int = 0,
) {
val spec: String
get() = when (type) {
ResizeType.None -> type.toString()
else -> "$type,$size"
}
override fun toString() = "ResizeConfig($spec)"
}
private fun PointF.limitBySqPixel(aspect: Float, maxSqPixels: Float): PointF {
val currentSqPixels = x * y
return when {
maxSqPixels <= 0 -> this
currentSqPixels <= maxSqPixels -> this
else -> {
val y = sqrt(maxSqPixels / aspect)
val x = aspect * y
PointF(x, y)
}
}
}
fun createResizedBitmap(
context: Context,
uri: Uri,
sizeLongSide: Int,
serverMaxSqPixel: Int? = null,
skipIfNoNeedToResizeAndRotate: Boolean = false,
) = createResizedBitmap(
context,
uri,
when {
sizeLongSide <= 0 -> ResizeConfig(ResizeType.None, 0)
else -> ResizeConfig(ResizeType.LongSide, sizeLongSide)
},
serverMaxSqPixel = serverMaxSqPixel,
skipIfNoNeedToResizeAndRotate = skipIfNoNeedToResizeAndRotate
)
fun createResizedBitmap(
context: Context,
// contentResolver.openInputStream に渡すUri
uri: Uri,
// リサイズ指定
resizeConfig: ResizeConfig,
// サーバ側の最大平方ピクセル
serverMaxSqPixel: Int? = null,
// 真の場合、リサイズも回転も必要ないならnullを返す
skipIfNoNeedToResizeAndRotate: Boolean = false,
): Bitmap? {
try {
val orientation: Int? = context.contentResolver.openInputStream(uri)?.use {
it.imageOrientation()
}
// 画像のサイズを調べる
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
options.inScaled = false
options.outWidth = 0
options.outHeight = 0
context.contentResolver.openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, options)
}
var srcWidth = options.outWidth
var srcHeight = options.outHeight
if (srcWidth <= 0 || srcHeight <= 0) {
context.showToast(false, "could not get image bounds.")
return null
}
// 回転後のサイズ
val srcSize = rotateSize(orientation, srcWidth.toFloat(), srcHeight.toFloat())
val aspect = srcSize.x / srcSize.y
/// 出力サイズの計算
val sizeSpec = resizeConfig.size.toFloat()
var dstSize: PointF = when (resizeConfig.type) {
ResizeType.None -> srcSize
ResizeType.LongSide ->
if (max(srcSize.x, srcSize.y) <= resizeConfig.size) {
srcSize
} else {
if (aspect >= 1f) {
PointF(
resizeConfig.size.toFloat(),
sizeSpec / aspect
)
} else {
PointF(
sizeSpec * aspect,
resizeConfig.size.toFloat()
)
}
}
ResizeType.SquarePixel -> srcSize.limitBySqPixel(aspect, sizeSpec * sizeSpec)
}
if (serverMaxSqPixel != null && serverMaxSqPixel > 0) {
dstSize = dstSize.limitBySqPixel(aspect, serverMaxSqPixel.toFloat())
}
val dstSizeInt = Point(
max(1, (dstSize.x + 0.5f).toInt()),
max(1, (dstSize.y + 0.5f).toInt())
)
val resizeRequired = dstSizeInt.x != srcSize.x.toInt() || dstSizeInt.y != srcSize.y.toInt()
log.i("createResizedBitmap: rc=${
resizeConfig
}, src=${
srcSize
}, dst=${
dstSizeInt
}, ori=${
orientation
}, resizeRequired=${
resizeRequired
}")
// リサイズも回転も必要がない場合
if (skipIfNoNeedToResizeAndRotate &&
(orientation == null || orientation == 1) &&
!resizeRequired
) {
log.w("createResizedBitmap: no need to resize or rotate.")
return null
}
// 長辺
val dstMax = max(dstSize.x, dstSize.y).toInt()
// inSampleSizeを計算
var bits = 0
var x = max(srcSize.x, srcSize.y).toInt()
while (x > 512 && x > dstMax * 2) {
++bits
x = x shr 1
}
options.inJustDecodeBounds = false
options.inSampleSize = 1 shl bits
val sourceBitmap: Bitmap? =
context.contentResolver.openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, options)
}
if (sourceBitmap == null) {
context.showToast(false, "could not decode image.")
return null
}
try {
// サンプル数が変化している
srcWidth = options.outWidth
srcHeight = options.outHeight
val scale = dstMax.toFloat() / max(srcWidth, srcHeight)
val matrix = Matrix().apply {
reset()
// 画像の中心が原点に来るようにして
postTranslate(srcWidth * -0.5f, srcHeight * -0.5f)
// スケーリング
postScale(scale, scale)
// 回転情報があれば回転
resolveOrientation(orientation)
// 表示領域に埋まるように平行移動
postTranslate(dstSizeInt.x.toFloat() * 0.5f, dstSizeInt.y.toFloat() * 0.5f)
}
// 出力用Bitmap作成
var dst: Bitmap? =
Bitmap.createBitmap(dstSizeInt.x, dstSizeInt.y, Bitmap.Config.ARGB_8888)
try {
return if (dst == null) {
context.showToast(false, "bitmap creation failed.")
null
} else {
val canvas = Canvas(dst)
val paint = Paint()
paint.isFilterBitmap = true
canvas.drawBitmap(sourceBitmap, matrix, paint)
log.d("createResizedBitmap: resized to ${dstSizeInt.x}x${dstSizeInt.y}")
val tmp = dst
dst = null
tmp
}
} finally {
dst?.recycle()
}
} finally {
sourceBitmap.recycle()
}
} catch (ex: FileNotFoundException) {
log.w(ex, "not found. $uri")
} catch (ex: SecurityException) {
log.w(ex, "maybe we need pick up image again.")
} catch (ex: Throwable) {
log.e(ex, "createResizedBitmap failed.")
}
return null
}