314 lines
9.6 KiB
Kotlin
314 lines
9.6 KiB
Kotlin
package jp.juggler.util.media
|
|
|
|
//import it.sephiroth.android.library.exif2.ExifInterface
|
|
import android.content.ContentResolver
|
|
import android.content.Context
|
|
import android.graphics.Bitmap
|
|
import android.graphics.BitmapFactory
|
|
import android.graphics.Canvas
|
|
import android.graphics.Matrix
|
|
import android.graphics.Paint
|
|
import android.graphics.Point
|
|
import android.graphics.PointF
|
|
import android.net.Uri
|
|
import androidx.annotation.StringRes
|
|
import androidx.exifinterface.media.ExifInterface
|
|
import jp.juggler.util.data.notEmpty
|
|
import jp.juggler.util.log.LogCategory
|
|
import jp.juggler.util.log.showToast
|
|
import java.io.FileNotFoundException
|
|
import java.io.InputStream
|
|
import kotlin.math.max
|
|
import kotlin.math.min
|
|
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,
|
|
canSkip = skipIfNoNeedToResizeAndRotate
|
|
)
|
|
|
|
fun Uri.bitmapMimeType(contentResolver: ContentResolver): String? =
|
|
try {
|
|
val options = BitmapFactory.Options()
|
|
options.inJustDecodeBounds = true
|
|
options.inScaled = false
|
|
contentResolver.openInputStream(this)?.use {
|
|
BitmapFactory.decodeStream(it, null, options)
|
|
}
|
|
options.outMimeType?.notEmpty()
|
|
} catch (ex: Throwable) {
|
|
log.w(ex, "bitmapMimeType: can't check bitmap mime type.")
|
|
null
|
|
}
|
|
|
|
fun createResizedBitmap(
|
|
context: Context,
|
|
|
|
// contentResolver.openInputStream に渡すUri
|
|
uri: Uri,
|
|
|
|
// リサイズ指定
|
|
resizeConfig: ResizeConfig,
|
|
|
|
// サーバ側の最大平方ピクセル
|
|
serverMaxSqPixel: Int? = null,
|
|
|
|
// 真の場合、リサイズも回転も必要ないならnullを返す
|
|
canSkip: 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.SquarePixel ->
|
|
srcSize.limitBySqPixel(aspect, sizeSpec * sizeSpec)
|
|
|
|
ResizeType.LongSide -> when {
|
|
max(srcSize.x, srcSize.y) <= resizeConfig.size -> srcSize
|
|
aspect >= 1f -> PointF(sizeSpec, sizeSpec / aspect)
|
|
else -> PointF(sizeSpec * aspect, sizeSpec)
|
|
}
|
|
}
|
|
|
|
if (serverMaxSqPixel != null && serverMaxSqPixel > 0) {
|
|
dstSize = dstSize.limitBySqPixel(aspect, serverMaxSqPixel.toFloat())
|
|
}
|
|
|
|
var 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 (canSkip &&
|
|
!resizeRequired &&
|
|
(orientation == null || orientation == 1)
|
|
) {
|
|
log.w("createResizedBitmap: no need to resize or rotate.")
|
|
return null
|
|
}
|
|
|
|
// リサイズする場合、ビットマップサイズ上限の成約がある
|
|
if (max(dstSizeInt.x, dstSizeInt.y) > 4096) {
|
|
val scale = 4096f / max(dstSizeInt.x, dstSizeInt.y).toFloat()
|
|
dstSize = PointF(
|
|
min(4096f, dstSize.x * scale),
|
|
min(4096f, dstSize.y * scale),
|
|
)
|
|
dstSizeInt = Point(
|
|
max(1, (dstSize.x + 0.5f).toInt()),
|
|
max(1, (dstSize.y + 0.5f).toInt())
|
|
)
|
|
}
|
|
|
|
// 長辺
|
|
val dstMax = min(4096, max(dstSize.x, dstSize.y).toInt())
|
|
|
|
// inSampleSizeを計算
|
|
var bits = 0
|
|
var n = max(srcSize.x, srcSize.y).toInt()
|
|
while (n > 4096 || (n > 512 && n > dstMax * 2)) {
|
|
++bits
|
|
n = n 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 matrix = Matrix().apply {
|
|
reset()
|
|
|
|
// 画像の中心が原点に来るようにして
|
|
postTranslate(srcWidth * -0.5f, srcHeight * -0.5f)
|
|
|
|
// スケーリング
|
|
val scale = dstMax.toFloat() / max(srcWidth, srcHeight)
|
|
postScale(scale, scale)
|
|
|
|
// 回転情報があれば回転
|
|
resolveOrientation(orientation)
|
|
|
|
// 表示領域に埋まるように平行移動
|
|
postTranslate(dstSizeInt.x.toFloat() * 0.5f, dstSizeInt.y.toFloat() * 0.5f)
|
|
}
|
|
|
|
// 出力用Bitmap作成
|
|
val dst = Bitmap.createBitmap(dstSizeInt.x, dstSizeInt.y, Bitmap.Config.ARGB_8888)
|
|
try {
|
|
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}")
|
|
return dst
|
|
} catch (ex: Throwable) {
|
|
dst.recycle()
|
|
throw ex
|
|
}
|
|
} 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
|
|
}
|