Android 图片处理工具类 ImageUtils

import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.graphics.Bitmap.CompressFormat.JPEG
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.util.Base64
import android.util.Log
import android.util.Pair
import androidx.exifinterface.media.ExifInterface
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min

/**
 * @author ganmin.he
 * @date 2021/6/29
 */
object ImageUtils {
    private const val TAG = "ImageUtils"

    @JvmStatic
    private val dateFormat by lazy {
        object : ThreadLocal<SimpleDateFormat>() {
            override fun initialValue(): SimpleDateFormat {
                return SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
            }
        }
    }

    @JvmStatic
    @Throws(IOException::class)
    fun createImageFile(): File {
        // Create an image file name
        val timeStamp = System.currentTimeMillis().let {
            dateFormat.get()?.format(Date(it)) ?: it.toString()
        }
        val imageFileName = "JPEG_" + timeStamp + "_"
        val storageDir = MyApplication.INSTANCE.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        return File.createTempFile(imageFileName, ".jpg", storageDir)
    }

    /**
     * @param reset if true, reset this InputStream after using it, otherwise close it.
     *              Only when this InputStream supports reset can reset be set to true,
     *              otherwise an IOException will be thrown.
     * @throws IOException if reset is true but this InputStream does not support mark/reset.
     *                     Or if reset is false but closing this InputStream throws an IOException.
     */
    @JvmStatic
    @Throws(IOException::class)
    private fun <T : InputStream, R> T.use(reset: Boolean = false, block: (T) -> R): R =
        if (reset) {
            if (!markSupported()) throw IOException("$this reset not supported")
            mark(Int.MAX_VALUE)
            block(this).also {
                reset()
            }
        } else use(block)

    @JvmStatic
    @Throws(FileNotFoundException::class, IOException::class)
    private fun Uri.openInputStream(): InputStream {
        val resolver = MyApplication.INSTANCE.contentResolver
        return resolver.openInputStream(this)
            ?: throw IOException("openInputStream($this) returned null")
    }

    // region decodeOrientation
    /**
     * @param reset if true, reset the InputStream obtained by getInputStream after using it,
     *              otherwise close it.
     *              Only when the InputStream obtained by getInputStream supports reset
     *              can reset be set to true, otherwise an IOException will be thrown.
     */
    @JvmStatic
    private fun decodeOrientation(
        reset: Boolean = false,
        getInputStream: () -> InputStream
    ): Int =
        try {
            getInputStream().use(reset) {
                val exif = ExifInterface(it)
                val orientation = exif.getAttributeInt(
                    ExifInterface.TAG_ORIENTATION,
                    ExifInterface.ORIENTATION_UNDEFINED
                )
                when (orientation) {
                    ExifInterface.ORIENTATION_ROTATE_90 -> 90
                    ExifInterface.ORIENTATION_ROTATE_180 -> 180
                    ExifInterface.ORIENTATION_ROTATE_270 -> 270
                    else -> 0
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "decodeOrientation error", e)
            0
        }

    @JvmStatic
    fun decodeOrientation(uri: Uri) =
        decodeOrientation {
            uri.openInputStream()
        }

    @JvmStatic
    fun decodeOrientation(photo: File) =
        decodeOrientation {
            photo.inputStream()
        }
    // endregion

    // region decodeSize
    /**
     * @param reset if true, reset the InputStream obtained by getInputStream after using it,
     *              otherwise close it.
     *              Only when the InputStream obtained by getInputStream supports reset
     *              can reset be set to true, otherwise an IOException will be thrown.
     */
    @JvmStatic
    @Throws(FileNotFoundException::class, IOException::class)
    private fun decodeSize(
        reset: Boolean = false,
        getInputStream: () -> InputStream
    ): Pair<Int, Int> =
        getInputStream().use(reset) {
            val options = BitmapFactory.Options()
            options.inJustDecodeBounds = true
            BitmapFactory.decodeStream(it, null, options)
            Pair(options.outWidth, options.outHeight)
        }

    @JvmStatic
    @Throws(FileNotFoundException::class, IOException::class)
    fun decodeSize(uri: Uri) =
        decodeSize {
            uri.openInputStream()
        }

    @JvmStatic
    @Throws(FileNotFoundException::class, IOException::class)
    fun decodeSize(photo: File) =
        decodeSize {
            photo.inputStream()
        }
    // endregion

    // region Bitmap extension functions
    /**
     * @param recycle whether to recycle this bitmap before returning.
     */
    @JvmStatic
    @JvmOverloads
    fun Bitmap.getByteArray(format: CompressFormat = JPEG, recycle: Boolean = true): ByteArray? =
        try {
            val byteArray = ByteArrayOutputStream()
            compress(format, 100, byteArray)
            byteArray.toByteArray()
        } catch (e: Exception) {
            Log.e(TAG, "getByteArray error", e)
            null
        } finally {
            if (recycle) recycle()
        }

    /**
     * @param recycle whether to recycle this bitmap before returning.
     */
    @JvmStatic
    @JvmOverloads
    fun Bitmap.getBase64(format: CompressFormat = JPEG, recycle: Boolean = true): String? {
        return try {
            val bytes = getByteArray(format, recycle) ?: return null
            Base64.encodeToString(bytes, Base64.NO_WRAP)
        } catch (e: Exception) {
            Log.e(TAG, "getBase64 error", e)
            null
        }
    }
    // endregion

    // region getBase64
    @JvmStatic
    private fun <T> T.getBase64Inner(
        maxDimension: Int,
        format: CompressFormat = JPEG,
        getInputStream: T.() -> InputStream
    ): String? {
        return try {
            decodeBitmap(maxDimension, maxDimension, getInputStream)
                ?.getBase64(format, true)
        } catch (e: Exception) {
            Log.e(TAG, "getBase64Inner error", e)
            null
        }
    }

    @JvmStatic
    @JvmOverloads
    fun getBase64(uri: Uri, maxDimension: Int, format: CompressFormat = JPEG) =
        uri.getBase64Inner(maxDimension, format) {
            openInputStream()
        }

    @JvmStatic
    @JvmOverloads
    fun getBase64(photo: File, maxDimension: Int, format: CompressFormat = JPEG) =
        photo.getBase64Inner(maxDimension, format) {
            inputStream()
        }

    @JvmStatic
    @JvmOverloads
    fun getBase64(photoPath: String, maxDimension: Int, format: CompressFormat = JPEG) =
        File(photoPath).let {
            it.getBase64Inner(maxDimension, format) {
                inputStream()
            }
        }
    // endregion

    @JvmStatic
    fun decodeBitmap(base64: String): Bitmap? =
        try {
            val data = Base64.decode(base64, Base64.DEFAULT)
            BitmapFactory.decodeByteArray(data, 0, data.size)
        } catch (e: Exception) {
            Log.e(TAG, "decodeBitmap error", e)
            null
        }

    /**
     * @param recycleIfResized whether to recycle this bitmap before returning another bitmap.
     */
    @JvmStatic
    @JvmOverloads
    fun Bitmap.resizeBitmap(maxDimension: Int, recycleIfResized: Boolean = false): Bitmap {
        val oriWidth = width
        val oriHeight = height
        if (maxDimension >= oriWidth && maxDimension >= oriHeight) {
            // ScaleSize > size of image => Do nothing
            return this
        } else {
            val newWidth: Int
            val newHeight: Int
            when {
                oriHeight > oriWidth -> {
                    newHeight = maxDimension
                    newWidth = newHeight * oriWidth / oriHeight
                }
                oriWidth > oriHeight -> {
                    newWidth = maxDimension
                    newHeight = newWidth * oriHeight / oriWidth
                }
                else -> {
                    newHeight = maxDimension
                    newWidth = maxDimension
                }
            }
            var recycleThis = recycleIfResized
            try {
                return Bitmap.createScaledBitmap(this, newWidth, newHeight, true).also {
                    recycleThis = (recycleIfResized && it !== this)
                }
            } finally {
                if (recycleThis) recycle()
            }
        }
    }

    // region decodeBitmap
    @JvmStatic
    fun decodeBitmap(uri: Uri, maxDimension: Int) =
        decodeBitmap(uri, maxDimension, maxDimension)

    @JvmStatic
    fun decodeBitmap(uri: Uri, maxWidth: Int, maxHeight: Int) =
        uri.decodeBitmap(maxWidth, maxHeight) {
            openInputStream()
        }

    @JvmStatic
    fun decodeBitmap(photo: File, maxDimension: Int) =
        decodeBitmap(photo, maxDimension, maxDimension)

    @JvmStatic
    fun decodeBitmap(photo: File, maxWidth: Int, maxHeight: Int) =
        photo.decodeBitmap(maxWidth, maxHeight) {
            inputStream()
        }

    @JvmStatic
    fun decodeBitmap(photoPath: String, maxDimension: Int) =
        decodeBitmap(photoPath, maxDimension, maxDimension)

    @JvmStatic
    fun decodeBitmap(photoPath: String, maxWidth: Int, maxHeight: Int) =
        File(photoPath).let {
            it.decodeBitmap(maxWidth, maxHeight) {
                inputStream()
            }
        }

    @JvmStatic
    private fun <T> T.decodeBitmap(
        maxWidth: Int, maxHeight: Int,
        getInputStream: T.() -> InputStream
    ): Bitmap? {
        var bitmap: Bitmap? = null
        var bufferedInputStream: BufferedInputStream? = null
        try {
            if (maxWidth <= 0 || maxHeight <= 0) {
                throw IllegalArgumentException(
                    "Invalid parameter: src=$this maxWidth=$maxWidth maxHeight=$maxHeight"
                )
            }
            bufferedInputStream = getInputStream().let {
                if (it is BufferedInputStream) it else BufferedInputStream(it)
            }
            // Get the dimensions of the image
            val size = decodeSize(true) { bufferedInputStream }
            val photoWidth = size.first
            val photoHeight = size.second
            if (photoWidth < 0 || photoHeight < 0) {
                throw IllegalStateException(
                    "Can not decode bounds: src=$this photoWidth=$photoWidth photoHeight=$photoHeight"
                )
            }
            // Get the orientation of the image
            val degree = decodeOrientation(true) { bufferedInputStream }
            // Determine how much to scale down the image
            val sampleSize = floor(
                when (degree) {
                    0, 180 -> max(
                        photoWidth.toFloat() / maxWidth.toFloat(),
                        photoHeight.toFloat() / maxHeight.toFloat()
                    )
                    else -> max(
                        photoHeight.toFloat() / maxWidth.toFloat(),
                        photoWidth.toFloat() / maxHeight.toFloat()
                    )
                }
            ).toInt()
            val options = BitmapFactory.Options().apply {
                /**
                 * Any inSampleSize <= 1 is treated the same as 1.
                 * On higher Android version, the decoder uses a final value based on inSampleSize.
                 * On lower Android version, the decoder uses a final value based on powers of 2,
                 * any other value will be rounded down to the nearest power of 2.
                 */
                inSampleSize = sampleSize
                @SuppressLint("ObsoleteSdkInt")
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                    inPurgeable = true
                }
            }
            bitmap = bufferedInputStream.use(true) {
                BitmapFactory.decodeStream(it, null, options)
                    ?: throw IllegalStateException("Can not decode bitmap: src=$this")
            }
            // Check if need to rescale
            val scaleFactor = when (degree) {
                0, 180 -> min(
                    maxWidth.toFloat() / bitmap.width.toFloat(),
                    maxHeight.toFloat() / bitmap.height.toFloat()
                )
                else -> min(
                    maxWidth.toFloat() / bitmap.height.toFloat(),
                    maxHeight.toFloat() / bitmap.width.toFloat()
                )
            }
            val needScale = scaleFactor < 1f
            // Rotate the image if necessary
            return if (degree == 0 && !needScale) {
                bitmap
            } else {
                val matrix = Matrix().apply {
                    if (needScale) setScale(scaleFactor, scaleFactor)
                    // postRotate after setScale !
                    if (degree != 0) postRotate(degree.toFloat())
                }
                Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true).also {
                    if (it !== bitmap) bitmap.recycle()
                }
            }
        } catch (e: Exception) {
            bitmap?.recycle()
            Log.e(TAG, "decodeBitmap error", e)
            return null
        } finally {
            try {
                bufferedInputStream?.close()
            } catch (e: IOException) {
                Log.e(TAG, "decodeBitmap close bufferedInputStream error", e)
            }
        }
    }
    // endregion
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值