compressor 是一个 Android 平台上的开源图片压缩库,使用它,可以方便的对本地图片进行压缩,与此同时,该库还提供了各种压缩参数的设置选项。
使用
val compressedImageFile = Compressor.compress(context, actualImageFile) {
resolution(1280, 720)
quality(80)
format(Bitmap.CompressFormat.WEBP)
size(2_097_152) // 2 MB
}
输入:
- 一个图片文件
- 压缩质量,以及格式化类型、最大压缩质量。
输出:
- 压缩后的图片文件(该文件默认存储应用的沙盒目录下)
核心
下面分析一下这个仓库的核心点。
压缩实现
该库的功能为压缩图片,具体压缩是通过 Bitmap 自身提供的 compress 方法进行压缩。
bitmap.compress(format, quality, fileOutputStream)
压缩参数组合
压缩操作是通过单例类 Compressor
的 compress
方法入口来完成,具体需要先指定目标压缩文件,然后指定压缩参数。
而压缩参数控制是通过第四个参数 compressionPatch
控制,它有一个默认的实现 DefaultConstraint.default()
,所以如果不指定其他设置,默认设置就会生效。
此外,当设置了自定义的压缩参数设置,这些参数设置项都会保存在 Compression
的 constraints
集合中,这是一个图片压缩参数的抽象接口集合,然后遍历参数集合,并调用不同的压缩参数实现,如下所示,这里是一种链式调用效果:
compression.constraints.forEach { constraint ->
//该策略是否满足条件
while (constraint.isSatisfied(result).not()) {
//如果不满足,就进行处理
result = constraint.satisfy(result)
}
}
这样每一个压缩参数的实现结果,都会作为接下来压缩参数的输入,从而达到链式调用的效果,一步一步,让所有的参数设置在一个图片源文件上生效。
压缩参数接口
Constraint
接口是该库的核心,也是一个很巧妙的设计。
通常来讲,对于图片压缩,我们可以按照面向过程的思想,只需要定义一个方法,然后在方法中对图片压缩质量、压缩格式、输出位置等按个进行处理,最终进行压缩,这样代码逻辑就会集中在一块里,这样的设计对后续代码的维护并不好,而且不具备模块性,整个是一个大块,看着也不是很优雅。
该库通过 Constraint
的接口很优雅的解决了这个问题。
不同的压缩参数,自己去实现自己的压缩方案,这个接口提供了两个方法:
interface Constraint {
fun isSatisfied(imageFile: File): Boolean
fun satisfy(imageFile: File): File
}
第一个方法是 isSatisfied
,它用于判断当前图片文件是否已经满足参数设置条件,如果已经满足,就不执行 satisfy
方法,否则就执行 satisfy
方法,该方法完成具体的压缩设置操作。
比如 FormatConstraint
的实现,这是指定压缩格式的实现类,如果当前图片已经是指定的格式,就进行处理,否则不处理。
class FormatConstraint(private val format: Bitmap.CompressFormat) : Constraint {
override fun isSatisfied(imageFile: File): Boolean {
return format == imageFile.compressFormat()
}
override fun satisfy(imageFile: File): File {
return overWrite(imageFile, loadBitmap(imageFile), format)
}
}
这里当检测到当前图片的格式不是指定的格式,就会执行 satisfy 方法,satisfy 方法中执行具体的压缩,纵观其他几个参数策略的实现,它们大都是通过 overWriter 去进行具体的图片参数设置。
overWrite 的实现
- 检查图片格式是否跟指定格式一致,否则更改图片名称后缀
- 删除临时的本地图片文件
- 使用新参数对 Bitmap 进行压缩、处理,并保存到新的临时文件并返回
代码如下所示:
fun overWrite(imageFile: File, bitmap: Bitmap, format: Bitmap.CompressFormat = imageFile.compressFormat(), quality: Int = 100): File {
val result = if (format == imageFile.compressFormat()) {
imageFile
} else {
File("${imageFile.absolutePath.substringBeforeLast(".")}.${format.extension()}")
}
imageFile.delete()
saveBitmap(bitmap, result, format, quality)
return result
}
saveBitmap
方法具体就是调用 Bitmap 的 compress 方法进行压缩。
拆分
- Constraint 压缩参数设置的抽象接口,每一种压缩策略必须实现该接口
- Compressor 压缩门面类,入口类,只提供一个 方法,用于让调用者设置不同的压缩选项,并启动压缩。
- Compression 一个用于盛放不同 Constraint 的集合
- 不同压缩策略的实现类
- DefaultConstraint 默认压缩参数的实现
- DestinationConstraint 指定压缩文件输出的文件位置
- FormatConstraint 指定文件最终输出的压缩格式
- QualityConstraint 指定压缩质量
- ResolutionConstraint 指定图片宽高值
- SizeConstraint 指定图片最终的压缩大小
细节
- 压缩质量设置对 PNG图片无效。
这是由于 Bitmap 自身的压缩限制,它提供的 compress 方法,即使设置了压缩质量,但是对 PNG 格式无效。
from Bitmap#compress 参数介绍
- 如何实现指定大小的压缩 #SizeConstraint
设置文件最大质量,如果当前文件大小大于最大质量,则继续进行压缩,具体通过设置图片采样率进行压缩,并设置最低采样率为10,另外设置了压缩次数,如果超过了指定的压缩次数,还没有达到大小,则不再压缩,技即使图片质量还没有达到目标值。
不足
从上面 overWrite 方法的实现可以看到,每一次压缩参数的生效,都会伴随上一个缓存文件的删除,以及下一个临时文件的生成,这样可能导致压缩会产生比较多的 IO 开销。
但这是一种博弈,这样的好处,是把不同的压缩参数实现拆分到不同的模块类,让代码结构更清晰,而且在我开发咕咚云图(一个手机图床)的过程中,并没有发现 IO 开销导致什么问题,所以,相比这样设计为代码带来的简洁以及可维护性,这样的 IO 开销可以忽略。
咕咚 DeepSource 2020/12/03