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
}
Android 图片处理工具类 ImageUtils
于 2021-09-30 16:11:01 首次发布