Bitmap压缩工具类: ImageCompressor.kt
Bitmap压缩工具类: ImageCompressor.kt
更好的方案 👉 https://juejin.im/post/6854573214451335175
import android.content.res.Resources
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.NinePatchDrawable
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import com.ando.toolkit.L
import java.io.*
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
/**
* 图片压缩实现类 From 任玉刚《Android开发艺术探索》 & https://github.com/chandilsachin/DietTracker
*
* 将图片加载进内存需要考虑的一些因素:
* 1. 在内存中完整加载图片的估计内存使用量
* 2. 根据应用的其他内存要求,愿意分配用于加载此图片的内存量
* 3. 图片要载入到的目标View的尺寸
* 4. 当前设备的屏幕大小和密度
*
* 核心思想是通过 BitmapFactory.Options 设置采样率,按需加载缩小后的图片,降低内存占用
*
* 对于 1024*1024 像素的图片来说,假设采用 ARGB8888 存储,占用内存为4MB。如果inSampleSize为2,采样后的图片内存占用为512*512,即1MB。
*
* Created by javakam on 2016年7月30日17:21:19 .
*/
object ImageCompressor {
/**
* Luban -> https://github.com/Curzibn/Luban/blob/master/library/src/main/java/top/zibin/luban/Engine.java
*/
private fun calculateInSampleSizeLuban(
reqWidth: Int,
reqHeight: Int
): Int {
L.w("calculateInSampleSize origin, w= $reqWidth h=$reqHeight")
val srcWidth = if (reqWidth % 2 == 1) reqWidth + 1 else reqWidth
val srcHeight = if (reqHeight % 2 == 1) reqHeight + 1 else reqHeight
val longSide: Int = max(srcWidth, srcHeight)
val shortSide: Int = min(srcWidth, srcHeight)
val scale = shortSide.toFloat() / longSide
return if (scale <= 1 && scale > 0.5625) {
if (longSide < 1664) {
1
} else if (longSide < 4990) {
2
} else if (longSide in 4991..10239) {
4
} else {
if (longSide / 1280 == 0) 1 else longSide / 1280
}
} else if (scale <= 0.5625 && scale > 0.5) {
if (longSide / 1280 == 0) 1 else longSide / 1280
} else {
ceil(longSide / (1280.0 / scale)).toInt()
}
}
private fun rotatingImage(bitmap: Bitmap?, angle: Int): Bitmap? {
val matrix = Matrix()
matrix.postRotate(angle.toFloat())
return Bitmap.createBitmap(
bitmap ?: return null,
0,
0,
bitmap.width,
bitmap.height,
matrix,
true
)
}
@Throws(IOException::class)
fun compressLuban(inputStream: InputStream, tagImg: File, focusAlpha: Boolean): File? {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true //首先使用 inJustDecodeBounds = true 来测量尺寸,不分配内存
BitmapFactory.decodeStream(inputStream, null, options)
options.inSampleSize =
calculateInSampleSizeLuban(
options.outWidth,
options.outHeight
)
options.inJustDecodeBounds = false
L.w("inSampleSize= ${options.inSampleSize}")
var tagBitmap: Bitmap? = BitmapFactory.decodeStream(inputStream, null, options)
val stream = ByteArrayOutputStream()
if (ImageChecker.SINGLE.isJPG(inputStream)) {
tagBitmap = rotatingImage(
tagBitmap,
ImageChecker.SINGLE.getOrientation(inputStream)
)
}
tagBitmap?.compress(
if (focusAlpha) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 60, stream
)
tagBitmap?.recycle()
val fos = FileOutputStream(tagImg)
fos.write(stream.toByteArray())
fos.flush()
fos.close()
stream.close()
return tagImg
}
fun compressLuban(filePath: String): Bitmap? {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true //首先使用 inJustDecodeBounds = true 来测量尺寸,不分配内存
BitmapFactory.decodeFile(filePath, options)
options.inSampleSize =
calculateInSampleSizeLuban(
options.outWidth,
options.outHeight
)
options.inJustDecodeBounds = false
L.w("inSampleSize= ${options.inSampleSize}")
return BitmapFactory.decodeFile(filePath, options)
}
/**
* 根据图片原始尺寸和需求尺寸计算压缩比例。
* 采样率是2的幂次的原因是解码器使用的最终值将向下舍入为最接近的2的幂:https://developer.android.google.cn/reference/android/graphics/BitmapFactory.Options#inSampleSize
* @param options 图片的尺寸信息
* @param reqHeight 目标高度
* @param reqWidth 目标宽度
*/
fun calculateInSampleSize(
options: BitmapFactory.Options,
reqWidth: Int,
reqHeight: Int
): Int {
if (reqWidth == 0 || reqHeight == 0) {
return 1
}
val (height: Int, width: Int) = options.run { outHeight to outWidth }
L.w("calculateInSampleSize origin, w= $width h=$height")
var inSampleSize = 1 //采样率
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
L.w("inSampleSize= $inSampleSize")
return inSampleSize
}
/**
* 从本地资源文件加载Bitmap
*/
fun decodeSampleBitmapFromResource(
res: Resources,
resId: Int,
reqWidth: Int,
reqHeight: Int
): Bitmap {
return BitmapFactory.Options().run {
inJustDecodeBounds = true //首先使用inJustDecodeBounds = true来测量尺寸,不分配内存
BitmapFactory.decodeResource(res, resId, this)
//计算采样率
inSampleSize =
calculateInSampleSize(
this,
reqWidth,
reqHeight
)
//用设置的采样率进行解码
inJustDecodeBounds = false
BitmapFactory.decodeResource(res, resId, this)
}
}
/**
* 从字节数组加载Bitmap
*/
fun decodeSampleBitmapFromByteArray(
data: ByteArray,
offset: Int,
reqWidth: Int,
reqHeight: Int
): Bitmap {
return BitmapFactory.Options().run {
inJustDecodeBounds = true //首先使用inJustDecodeBounds = true来测量尺寸,不分配内存
BitmapFactory.decodeByteArray(data, offset, data.size, this)
//计算采样率
inSampleSize =
calculateInSampleSize(
this,
reqWidth,
reqHeight
)
//用设置的采样率进行解码
inJustDecodeBounds = false
BitmapFactory.decodeByteArray(data, offset, data.size, this)
}
}
/**
* 从文件路径加载Bitmap
*/
fun decodeSampleBitmapFromFile(
filePath: String,
reqWidth: Int,
reqHeight: Int
): Bitmap {
return BitmapFactory.Options().run {
inJustDecodeBounds = true //首先使用inJustDecodeBounds = true来测量尺寸,不分配内存
BitmapFactory.decodeFile(filePath, this)
//计算采样率
inSampleSize =
calculateInSampleSize(
this,
reqWidth,
reqHeight
)
//用设置的采样率进行解码
inJustDecodeBounds = false
BitmapFactory.decodeFile(filePath, this)
}
}
/**
* <h1>public static Bitmap decodeSampledBitmap(String filePath)</h1>
*
*
* Decodes bitmap of given size.
*
*
* @param filePath - file path of image.
* @return a bitmap of given width and height.
*/
fun decodeSampleBitmapFromFile(filePath: String?): Bitmap? {
// First decode with inJustDecodeBounds=true to check dimensions
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
// Calculate inSampleSize
options.inSampleSize =
calculateInSampleSize(
options, options.outWidth,
options.outHeight
)
L.w("inSampleSize= ${options.inSampleSize}")
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false
return BitmapFactory.decodeFile(filePath, options)
}
/**
* 从IO流加载Bitmap
*/
fun decodeSampleBitmapFromStream(
inputStream: InputStream,
outPadding: Rect,
reqWidth: Int,
reqHeight: Int
): Bitmap? {
return BitmapFactory.Options().run {
inJustDecodeBounds = true //首先使用inJustDecodeBounds = true来测量尺寸,不分配内存
BitmapFactory.decodeStream(inputStream, outPadding, this)
//计算采样率
inSampleSize =
calculateInSampleSize(
this,
reqWidth,
reqHeight
)
//用设置的采样率进行解码
inJustDecodeBounds = false
BitmapFactory.decodeStream(inputStream, outPadding, this)
}
}
/**
* 从IO流加载Bitmap
*/
fun decodeSampleBitmapFromFileDescriptor(
fd: FileDescriptor,
outPadding: Rect,
reqWidth: Int,
reqHeight: Int
): Bitmap? {
return BitmapFactory.Options().run {
inJustDecodeBounds = true //首先使用inJustDecodeBounds = true来测量尺寸,不分配内存
BitmapFactory.decodeFileDescriptor(fd, outPadding, this)
//计算采样率
inSampleSize =
calculateInSampleSize(
this,
reqWidth,
reqHeight
)
//用设置的采样率进行解码
inJustDecodeBounds = false
BitmapFactory.decodeFileDescriptor(fd, outPadding, this)
}
}
fun decodeSampledBitmapDrawable(
res: Resources?,
filePath: String?
): BitmapDrawable? {
// First decode with inJustDecodeBounds=true to check dimensions
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
// Calculate inSampleSize
options.inSampleSize =
calculateInSampleSize(
options, options.outWidth,
options.outHeight
)
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false
return BitmapDrawable(
res, BitmapFactory.decodeFile(
filePath,
options
)
)
}
fun decodeNinePatchDrawable(
res: Resources?,
filePath: String?
): NinePatchDrawable? {
// First decode with inJustDecodeBounds=true to check dimensions
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
// Calculate inSampleSize
options.inSampleSize =
calculateInSampleSize(
options, options.outWidth,
options.outHeight
)
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile(filePath, options)
val patch = NinePatch(bitmap, bitmap.ninePatchChunk, null)
return NinePatchDrawable(res, patch)
}
fun getNinePatchDrawable(
res: Resources?,
bitmap: Bitmap
): NinePatchDrawable? {
val patch = NinePatch(bitmap, bitmap.ninePatchChunk, null)
return NinePatchDrawable(res, patch)
}
fun decodeSampledBitmapDrawable(
res: Resources?, filePath: String?, heightInPixel: Int
): BitmapDrawable? {
// First decode with inJustDecodeBounds=true to check dimensions
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
val widthRatio = options.outWidth / options.outHeight.toFloat()
// Calculate inSampleSize
options.inSampleSize =
calculateInSampleSize(
options,
(widthRatio * heightInPixel).toInt(), heightInPixel
)
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false
val b = Bitmap.createScaledBitmap(
BitmapFactory.decodeFile(filePath, options),
(widthRatio * heightInPixel).toInt(), heightInPixel, true
)
return BitmapDrawable(res, b)
}
fun decodeScaledSampleBitmap(
res: Resources?, resourceId: Int, width: Int, height: Int
): Bitmap? {
// First decode with inJustDecodeBounds=true to check dimensions
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(res, resourceId, options)
// Calculate inSampleSize
options.inSampleSize =
calculateInSampleSize(
options,
width, height
)
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false
return Bitmap.createScaledBitmap(
BitmapFactory.decodeResource(res, resourceId, options),
width, height, true
)
}
fun decodeSampledBitmapDrawableInDp(
res: Resources,
filePath: String?, heightInPixel: Int
): BitmapDrawable? {
// First decode with inJustDecodeBounds=true to check dimensions
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
var widthRatio = 0f
try {
widthRatio = options.outWidth.toFloat() / options.outHeight
} catch (e: ArithmeticException) {
}
// Calculate inSampleSize
options.inSampleSize =
calculateInSampleSize(
options,
(widthRatio * heightInPixel).toInt() + 30, heightInPixel
)
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false
var b: Bitmap? = null
b = try {
Bitmap.createScaledBitmap(
BitmapFactory.decodeFile(
filePath,
options
),
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
((widthRatio * heightInPixel).toInt()).toFloat() ?: 0F,
res.displayMetrics
).toInt(), TypedValue
.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
heightInPixel.toFloat(), res.displayMetrics
).toInt(),
true
)
} catch (e: NullPointerException) {
return null
}
return BitmapDrawable(res, b)
}
fun decodeSampledBitmapInDp(
res: Resources,
filePath: String?, heightInPixel: Int
): Bitmap? {
// First decode with inJustDecodeBounds=true to check dimensions
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
var widthRatio = 0f
try {
widthRatio = options.outWidth / options.outHeight.toFloat()
} catch (e: ArithmeticException) {
}
// Calculate inSampleSize
options.inSampleSize =
calculateInSampleSize(
options,
(widthRatio * heightInPixel).toInt() + 30, heightInPixel
)
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false
var b: Bitmap? = null
b = try {
Bitmap.createScaledBitmap(
BitmapFactory.decodeFile(
filePath,
options
),
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
((widthRatio * heightInPixel).toInt()).toFloat() ?: 0F,
res.displayMetrics
).toInt(), TypedValue
.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
heightInPixel.toFloat(), res.displayMetrics
).toInt(),
true
)
} catch (e: NullPointerException) {
return null
}
return b
}
fun decodeBitmapDrawable(
res: Resources?,
filePath: String?
): BitmapDrawable? {
return BitmapDrawable.createFromPath(filePath) as BitmapDrawable?
}
/**
* <h1>public static Bitmap createBitmap(View v)</h1>
*
*
* Creates a bitmap from a view.
*
*/
fun createBitmap(v: View): Bitmap? {
return if (v.measuredHeight <= 0) {
v.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val b = Bitmap.createBitmap(
v.measuredWidth,
v.measuredHeight, Bitmap.Config.ARGB_8888
)
val c = Canvas(b)
v.layout(0, 0, v.measuredWidth, v.measuredHeight)
v.draw(c)
b
} else {
val b = Bitmap.createBitmap(
v.width, v.height,
Bitmap.Config.ARGB_8888
)
// Bitmap b =
// Bitmap.createBitmap(v.getLayoutParams().width,v.getLayoutParams().height,
// Bitmap.Config.ARGB_8888);
val c = Canvas(b)
v.layout(v.left, v.top, v.right, v.right)
v.draw(c)
b
}
}
}
ImageChecker.kt
import android.graphics.BitmapFactory
import com.ando.toolkit.L
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.util.*
import kotlin.experimental.and
enum class ImageChecker {
SINGLE;
private val JPEG_SIGNATURE = byteArrayOf(0xFF.toByte(), 0xD8.toByte(), 0xFF.toByte())
/**
* Determine if it is JPG.
*
* @param inputStream image file input stream
*/
fun isJPG(inputStream: InputStream?): Boolean {
return isJPG(toByteArray(inputStream))
}
/**
* Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
*/
fun getOrientation(`is`: InputStream?): Int {
return getOrientation(toByteArray(`is`))
}
private fun isJPG(data: ByteArray?): Boolean {
if (data == null || data.size < 3) {
return false
}
val signatureB = byteArrayOf(data[0], data[1], data[2])
return JPEG_SIGNATURE.contentEquals(signatureB)
}
private fun getOrientation(jpeg: ByteArray?): Int {
if (jpeg == null) {
return 0
}
var offset = 0
var length = 0
// ISO/IEC 10918-1:1993(E)
while (offset + 3 < jpeg.size && (jpeg[offset++] and (0xFF).toByte()) == (0xFF).toByte()) {
val marker: Int = (jpeg[offset] and (0xFF).toByte()).toInt()
// Check if the marker is a padding.
if (marker == 0xFF) {
continue
}
offset++
// Check if the marker is SOI or TEM.
if (marker == 0xD8 || marker == 0x01) {
continue
}
// Check if the marker is EOI or SOS.
if (marker == 0xD9 || marker == 0xDA) {
break
}
// Get the length and check if it is reasonable.
length = pack(jpeg, offset, 2, false)
if (length < 2 || offset + length > jpeg.size) {
L.e(TAG, "Invalid length")
return 0
}
// Break if the marker is EXIF in APP1.
if (marker == 0xE1 && length >= 8 && pack(
jpeg,
offset + 2,
4,
false
) == 0x45786966 && pack(jpeg, offset + 6, 2, false) == 0
) {
offset += 8
length -= 8
break
}
// Skip other markers.
offset += length
length = 0
}
// JEITA CP-3451 Exif Version 2.2
if (length > 8) {
// Identify the byte order.
var tag = pack(jpeg, offset, 4, false)
if (tag != 0x49492A00 && tag != 0x4D4D002A) {
L.e(TAG, "Invalid byte order")
return 0
}
val littleEndian = tag == 0x49492A00
// Get the offset and check if it is reasonable.
var count = pack(jpeg, offset + 4, 4, littleEndian) + 2
if (count < 10 || count > length) {
L.e(TAG, "Invalid offset")
return 0
}
offset += count
length -= count
// Get the count and go through all the elements.
count = pack(jpeg, offset - 2, 2, littleEndian)
while (count-- > 0 && length >= 12) {
// Get the tag and check if it is orientation.
tag = pack(jpeg, offset, 2, littleEndian)
if (tag == 0x0112) {
when (pack(jpeg, offset + 8, 2, littleEndian)) {
1 -> return 0
3 -> return 180
6 -> return 90
8 -> return 270
else -> {
}
}
L.e(TAG, "Unsupported orientation")
return 0
}
offset += 12
length -= 12
}
}
L.e(TAG, "Orientation not found")
return 0
}
fun extSuffix(inputStream: InputStream?): String {
return try {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, options)
options.outMimeType.replace("image/", ".")
} catch (e: Exception) {
JPG
}
}
fun needCompress(leastCompressSize: Int, path: String?): Boolean {
if (leastCompressSize > 0) {
val source = File(path)
return source.exists() && source.length() > leastCompressSize shl 10
}
return true
}
private fun pack(
bytes: ByteArray,
offset: Int,
length: Int,
littleEndian: Boolean
): Int {
var offset = offset
var length = length
var step = 1
if (littleEndian) {
offset += length - 1
step = -1
}
var value = 0
while (length-- > 0) {
value = value shl 8 or (bytes[offset] and (0xFF).toByte()).toInt()
offset += step
}
return value
}
private fun toByteArray(inputStream: InputStream?): ByteArray {
if (inputStream == null) {
return ByteArray(0)
}
val buffer = ByteArrayOutputStream()
var read: Int
val data = ByteArray(4096)
try {
while (inputStream.read(data, 0, data.size).also { read = it } != -1) {
buffer.write(data, 0, read)
}
} catch (ignored: Exception) {
return ByteArray(0)
} finally {
try {
buffer.close()
} catch (ignored: IOException) {
}
}
return buffer.toByteArray()
}
companion object {
private const val TAG = "Luban"
private const val JPG = ".jpg"
}
}

浙公网安备 33010602011771号