2018-12-01 00:02:18 +01:00
|
|
|
package jp.juggler.util
|
2018-01-21 13:46:36 +01:00
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
import android.graphics.*
|
2020-08-25 02:04:18 +02:00
|
|
|
import androidx.exifinterface.media.ExifInterface
|
2018-01-21 13:46:36 +01:00
|
|
|
import android.net.Uri
|
2021-02-09 23:25:06 +01:00
|
|
|
import androidx.annotation.StringRes
|
2020-08-25 02:04:18 +02:00
|
|
|
//import it.sephiroth.android.library.exif2.ExifInterface
|
2018-12-03 09:29:19 +01:00
|
|
|
import java.io.FileNotFoundException
|
2019-10-06 22:46:56 +02:00
|
|
|
import java.io.InputStream
|
2019-10-03 22:05:07 +02:00
|
|
|
import kotlin.math.max
|
|
|
|
import kotlin.math.sqrt
|
2018-01-21 13:46:36 +01:00
|
|
|
|
2019-10-03 22:05:07 +02:00
|
|
|
private val log = LogCategory("BitmapUtils")
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
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
|
|
|
|
}
|
2019-10-06 22:46:56 +02:00
|
|
|
|
2019-10-06 22:54:15 +02:00
|
|
|
// 回転情報の値に合わせて、wとhを入れ替える
|
2021-06-20 15:12:25 +02:00
|
|
|
fun rotateSize(orientation: Int?, w: Float, h: Float): PointF =
|
|
|
|
when (orientation) {
|
|
|
|
5, 6, 7, 8 -> PointF(h, w)
|
|
|
|
else -> PointF(w, h)
|
|
|
|
}
|
2019-10-03 22:05:07 +02:00
|
|
|
|
2019-10-06 22:54:15 +02:00
|
|
|
// 回転情報を解決するようにmatrixに回転を加える
|
2021-06-20 15:12:25 +02:00
|
|
|
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
|
2019-10-06 22:54:15 +02:00
|
|
|
}
|
|
|
|
|
2019-10-06 22:46:56 +02:00
|
|
|
enum class ResizeType {
|
2021-06-20 15:12:25 +02:00
|
|
|
// リサイズなし
|
|
|
|
None,
|
|
|
|
|
|
|
|
// 長辺がsize以下になるようリサイズ
|
|
|
|
LongSide,
|
|
|
|
|
|
|
|
// 平方ピクセルが size*size 以下になるようリサイズ
|
|
|
|
SquarePixel,
|
2018-01-21 13:46:36 +01:00
|
|
|
}
|
2018-01-21 17:47:13 +01:00
|
|
|
|
2019-10-03 22:05:07 +02:00
|
|
|
class ResizeConfig(
|
2021-06-20 15:12:25 +02:00
|
|
|
val type: ResizeType,
|
|
|
|
val size: Int,
|
|
|
|
@StringRes val extraStringId: Int = 0,
|
|
|
|
) {
|
|
|
|
val spec: String
|
|
|
|
get() = when (type) {
|
|
|
|
ResizeType.None -> type.toString()
|
|
|
|
else -> "$type,$size"
|
|
|
|
}
|
2021-02-09 23:25:06 +01:00
|
|
|
}
|
2019-10-03 22:05:07 +02:00
|
|
|
|
|
|
|
fun createResizedBitmap(
|
2021-06-20 15:12:25 +02:00
|
|
|
context: Context,
|
|
|
|
uri: Uri,
|
|
|
|
sizeLongSide: Int,
|
|
|
|
skipIfNoNeedToResizeAndRotate: Boolean = false
|
2019-10-06 22:46:56 +02:00
|
|
|
) =
|
2021-06-20 15:12:25 +02:00
|
|
|
createResizedBitmap(
|
|
|
|
context,
|
|
|
|
uri,
|
|
|
|
when {
|
|
|
|
sizeLongSide <= 0 -> ResizeConfig(ResizeType.None, 0)
|
|
|
|
else -> ResizeConfig(ResizeType.LongSide, sizeLongSide)
|
|
|
|
},
|
|
|
|
skipIfNoNeedToResizeAndRotate = skipIfNoNeedToResizeAndRotate
|
|
|
|
)
|
2019-10-03 22:05:07 +02:00
|
|
|
|
2018-01-21 17:47:13 +01:00
|
|
|
fun createResizedBitmap(
|
2021-06-20 15:12:25 +02:00
|
|
|
context: Context,
|
|
|
|
|
|
|
|
// contentResolver.openInputStream に渡すUri
|
|
|
|
uri: Uri,
|
|
|
|
|
|
|
|
// リサイズ指定
|
|
|
|
resizeConfig: ResizeConfig,
|
|
|
|
|
|
|
|
// 真の場合、リサイズも回転も必要ないなら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()
|
|
|
|
val 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 -> {
|
|
|
|
val maxPixels = sizeSpec * sizeSpec
|
|
|
|
val currentPixels = srcSize.x * srcSize.y
|
|
|
|
if (currentPixels <= maxPixels) {
|
|
|
|
srcSize
|
|
|
|
} else {
|
|
|
|
val y = sqrt(maxPixels / aspect)
|
|
|
|
val x = aspect * y
|
|
|
|
PointF(x, y)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
// リサイズも回転も必要がない場合
|
|
|
|
if (skipIfNoNeedToResizeAndRotate &&
|
|
|
|
(orientation == null || orientation == 1) &&
|
|
|
|
!resizeRequired
|
|
|
|
) {
|
|
|
|
log.d("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.trace(ex, "createResizedBitmap")
|
|
|
|
}
|
|
|
|
return null
|
2018-01-21 17:47:13 +01:00
|
|
|
}
|