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 }