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"
    }

}
posted @ 2020-08-18 14:45  javakam  阅读(428)  评论(0)    收藏  举报