图片压缩库 compressor | DS

咕咚 于 2020-12-20 发布

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)

压缩参数组合

通过单例类 Compressorcompress 方法入口先指定目标压缩文件,具体压缩参数控制是通过第四个参数 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 方法,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 的实现

代码如下所示:

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 方法进行压缩。

拆分

细节

这是由于 Bitmap 自身的压缩限制,它提供的 compress 方法,即使设置了压缩质量,但是对 PNG 格式无效。

from Bitmap#compress 参数介绍

设置文件最大质量,如果当前文件大小大于最大质量,则继续进行压缩,具体通过设置图片采样率进行压缩,并设置最低采样率为10,另外设置了压缩次数,如果超过了指定的压缩次数,还没有达到大小,则不再压缩,技即使图片质量还没有达到目标值。

不足

从上面 overWrite 方法的实现可以看到,每一次压缩参数的生效,都会伴随上一个缓存文件的删除,以及下一个临时文件的生成,这样可能导致压缩会产生比较多的 IO 开销。

但这是一种博弈,这样的好处,是把不同的压缩参数实现拆分到不同的模块类,让代码结构更清晰,而且在我开发咕咚云图(一个手机图床)的过程中,并没有发现 IO 开销导致什么问题,所以,相比这样设计为代码带来的简洁以及可维护性,这样的 IO 开销可以忽略。

咕咚 DeepSource 2020/12/03