package jp.juggler.util import android.content.Context import android.graphics.* import androidx.exifinterface.media.ExifInterface import android.net.Uri //import it.sephiroth.android.library.exif2.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 ) fun createResizedBitmap( context : Context, uri : Uri, sizeLongSide : Int, skipIfNoNeedToResizeAndRotate : Boolean = false ) = createResizedBitmap( context, uri, if(sizeLongSide <= 0) ResizeConfig(ResizeType.None, 0) else ResizeConfig(ResizeType.LongSide, sizeLongSide), skipIfNoNeedToResizeAndRotate = skipIfNoNeedToResizeAndRotate ) fun createResizedBitmap( 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 src_width = options.outWidth var src_height = options.outHeight if(src_width <= 0 || src_height <= 0) { showToast(context, false, "could not get image bounds.") return null } // 回転後のサイズ val srcSize = rotateSize(orientation, src_width.toFloat(), src_height.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) { showToast(context, false, "could not decode image.") return null } try { // サンプル数が変化している src_width = options.outWidth src_height = options.outHeight val scale = dstMax.toFloat() / max(src_width, src_height) val matrix = Matrix().apply { reset() // 画像の中心が原点に来るようにして postTranslate(src_width * - 0.5f, src_height * - 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) { showToast(context, 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 %sx%s", dstSizeInt.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 }