图片压缩库 compressor 学习

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(),所以如果不指定其他设置,默认设置就会生效。

此外,当设置了自定义的压缩参数设置,这些参数设置项都会保存在 Compressionconstraints 集合中,这是一个图片压缩参数的抽象接口集合,然后遍历参数集合,并调用不同的压缩参数实现,如下所示,这里是一种链式调用效果:

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

你可能感兴趣的:(图片压缩库 compressor 学习)