高效的 Android 图片压缩框架扩展库

4,254 阅读5分钟

本项目主要基于 Android 自带的图片压缩 API 进行实现,提供了开源压缩方案 Luban 和 Compressor 的实现,提供了 Fie 类型数据源的方式,并在它们的基础之上进行了功能上的拓展。该项目的主要目的在于:提供一个统一图片压缩框库的实现,集成常用的两种图片压缩算法,让你以更低的成本集成图片压缩功能到自己的项目中。

项目背景:开源的图片压缩库

现在 Github 上的图片压缩框架主要有 Luban 和 Compressor 两个。Star 的数量也比较高,一个 11K,另一个 4K. 但是,这两个图片压缩的库有各自的优点和缺点。下面我们通过一个表格总结一下:

框架 优点 缺点
Luban 据说是根据微信图片压缩逆推的算法。 1.只适用于一般的图片展示的场景,无法对图片的尺寸进行精准压缩;2.压缩策略单一,应用场景有限。
Compressor 1.可以对图片的尺寸进行精准压缩;2.支持同步或异步获取。 1.无法通过最佳采样量压缩图片,手动设置无法保证体积最小,质量无法保证。

以上,我们总结了 Github 上面的两款比较受欢迎的开源图片压缩库的优缺点。正如我们上面所说,我们希望能够综合它们的优点,并且解决以上两款开源库设计不好的地方。因此,你可以通过下文来了解我们的库所提供的功能,以及如何在您的项目中接入我们的压缩方案。

功能和特性

目前,我们的库已经支持了下面的功能,在后面的介绍中,我们会介绍如何在项目中进行详细配置和使用:

  • 支持 Luban 压缩方案:据介绍这是微信逆推的压缩算法,它在我们的项目中只作为一种可选的压缩方案,除此之外您还可以使用 Compressor 进行压缩。
  • 支持 Compressor 压缩方案:这种压缩方案综合了 Android 自带的三种压缩方式,可以对压缩结果的尺寸进行精确的控制。此外,在我们的项目中,我们对这种压缩方案的功能进行了拓展,还提供了多种可选的策略,用来对尺寸进行更详细的配置。
  • 支持 AsyncTask + 回调的压缩方式:这种方式通过使用 AsyncTask 在后台线程中执行压缩任务,当获取到压缩结果的时候通过 Handler 在主线程中返回压缩结果。
  • 支持 Kotlin 协程:引入了 Kotlin 协程,你可以在这个版本上面使用 Kotlin 协程进行压缩并获取结果。

提供了同步的压缩方式:当然,有时候我们并不需要使用回调或者 RxJava 执行异步任务。比如,当我们本身已经处于后台线程的时候,我们希望的只是在当前线程中直接执行压缩任务并拿到压缩结果。因此,为了让我们的库适用于更多的应用场景,我们提出了这种压缩方案。

鲁班压缩算法

目前做App开发总绕不开图片这个元素。但是随着手机拍照分辨率的提升,图片的压缩成为一个很重要的问题。单纯对图片进行裁切,压缩已经有很多文章介绍。但是裁切成多少,压缩成多少却很难控制好,裁切过头图片太小,质量压缩过头则显示效果太差。

于是自然想到App巨头“微信”会是怎么处理,Luban(鲁班)就是通过在微信朋友圈发送近100张不同分辨率图片,对比原图与微信压缩后的图片逆向推算出来的压缩算法。

1、判断图片比例值,是否处于以下区间内;

  • [1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内
  • [0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内
  • [0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内

2、判断图片最长边是否过边界值;

  • [1, 0.5625) 边界值为:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3)
  • [0.5625, 0.5) 边界值为:1280 * pow(2, n-1)(n≥1)
  • [0.5, 0) 边界值为:1280 * pow(2, n-1)(n≥1)

3、计算压缩图片实际边长值,以第2步计算结果为准,超过某个边界值则:width / pow(2, n-1),height/pow(2, n-1)

4、计算压缩图片的实际文件大小,以第2、3步结果为准,图片比例越大则文件越大。 size = (newW * newH) / (width * height) * m;

  • [1, 0.5625) 则 width & height 对应 1664,4990,1280 * n(n≥3),m 对应 150,300,300;
  • [0.5625, 0.5) 则 width = 1440,height = 2560, m = 200;
  • [0.5, 0) 则 width = 1280,height = 1280 / scale,m = 500;注:scale为比例值

5、判断第4步的size是否过小

  • [1, 0.5625) 则最小 size 对应 60,60,100
  • [0.5625, 0.5) 则最小 size 都为 100
  • [0.5, 0) 则最小 size 都为 100

6、将前面求到的值压缩图片 width, height, size 传入压缩流程,压缩图片直到满足以上数值

实践对比

为了比较扩展库的压缩功能是否正常,进行了10张网络上收集而来的图片(均大于1.5M),使用扩展库和 Compressor 和 Luban库进行对比,使用的压缩质量均为60(Luban默认为60,且无法更改)。

图片/原始大小 compressor luban 扩展库 compressor 模式 扩展库 luban 模式
p1/3.6M 116.06K 115.95K 116.06K 115.95K
p2/4.11M 71.74K 39.68K 71.74K 39.68K
p3/8.63M 18.32K 18.14K 18.32K 18.14K
p4/3.84M 160.44K 161.08K 160.44K 161.08K
p5/5.64M 171.26K 171.02K 171.26K 171.02K
p6/7.32M 163.2K 163.43K 163.2K 163.43K
p7/1.88M 17.14K 17.14K 17.14K 17.14K
p8/3.89M 96.68K 56.37K 96.68K 56.37K
p9/1.16M 113.26K 66.69K 113.26K 66.69K
p10/4.31M 278.76K 278.05K 278.76K 278.05K

由此可见,压缩效果是一致的。

使用

1、在项目的依赖中添加该库的依赖:

implementation 'com.mmc.plug:compressor:1.0.0-SNAPSHOT'

2、使用我们库进行压缩。 协程用法:

  • 1、compressor模式。
GlobalScope.launch(Dispatchers.Main) {
            val compressedImageFile = Compressor.compress(context, actualImageFile) {
                destination(File("${cachePath(context)}"))
                resolution(1280, 720)
                quality(60)
                format(Bitmap.CompressFormat.JPEG)
                size(2_097_152) // 2 MB
            }
        }
  • 2、luban模式。
GlobalScope.launch(Dispatchers.Main) {
            val compressedImageFile = Compressor.compress(context, actualImageFile) {
                destination(File("${cachePath(context)}"))
                luban(ignoreBySize)
            }
        }

同步用法:

Compress.with(context, actualImageFile)
            .setDestination(File("${cachePath(context)}"))
            .setResolution(1280, 720)
            .setQuality(60)
            .setFormat(Bitmap.CompressFormat.JPEG)
            .setSize(2_097_152) // 2 MB
            .get()

异步回调用法:

Compress.with(context, actualImageFile)
            .setDestination(File("${cachePath(context)}"))
            .setLubanSize(ignoreBySize)
            .setCompressListener(object : CompressListener {
                override fun onSuccess(compressedImageFile: File?) {
                    
                }

                override fun onError(throwable: Throwable?) {

                }

                override fun onStart() {

                }
            }).launch()

扩展库已经上传Maven,有需要的请自取。