compressor 是一個 Android 平臺上的開源圖片壓縮庫,使用它,可以方便的對本地圖片進行壓縮,並提供了各種壓縮參數設置。
使用
val compressedImageFile = Compressor.compress(context, actualImageFile) {
resolution(1280, 720)
quality(80)
format(Bitmap.CompressFormat.WEBP)
size(2_097_152) // 2 MB
}
輸入一個正常的圖片文件,並設置壓縮質量,以及格式化類型,最大質量,輸出壓縮後的圖片文件。
對於這種語法,還不太清楚這是是 kotlin 的什麼語法。
核心
壓縮實現
通過 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 方法,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 開銷導致什麼問題,所以這塊還好。