一、写在前面
图片压缩是一个老生常谈的问题,工作中也很常见。客户端同学会遇到这种情况,各种资源图,UI也不压缩,直接丢过来,我们也懒,直接放工程里,久而久之,越来越臃肿,突然某一天,老大说,我们的安装包怎么这么大,需要压缩一下吧。。。那就压缩吧。对于安装包瘦身,方式有很多,其实大部分都是从资源入手,压缩资源,通过proGuard剔除未使用的资源,麻烦点可以根据R文件内联,通过字节码插桩,修改字节码(Android中app模块是不使用R文件的,而是根据编译过程生成的R.txt,将资源名称直接改成对应的id,而moudle中还使用R文件)将对应名称替换成id,这样R文件为被使用,也可以通过proGuard剔除了。这是题外话,下面回归正题。
二、目标
如图,可以直接使用命令,输入对应的参数(没有参数,可以提示),直接根据输入的目录压缩里面的图片资源,不需要手动替换。一般的会生成新的目录,然后还需要手动去替换原目录。
三、开始吧
1、在Intellij IDEA中新建工程
如果要白嫖Intellij IDEA的,喏。。。 也许还没有过期
KQ8KMJ77TY-eyJsaWNlbnNlSWQiOiJLUThLTUo3N1RZIiwibGljZW5zZWVOYW1lIjoiVW5pdmVyc2l0YXMgTmVnZXJpIE1hbGFuZyIsImxpY2Vuc2VlVHlwZSI6IkNMQVNTUk9PTSIsImFzc2lnbmVlTmFtZSI6IkpldOWFqOWutuahtiDorqTlh4blupflkI0iLCJhc3NpZ25lZUVtYWlsIjoibmtucWFyY214a0AxNjMuY29tIiwibGljZW5zZVJlc3RyaWN0aW9uIjoiRm9yIGVkdWNhdGlvbmFsIHVzZSBvbmx5IiwiY2hlY2tDb25jdXJyZW50VXNlIjpmYWxzZSwicHJvZHVjdHMiOlt7ImNvZGUiOiJHTyIsInBhaWRVcFRvIjoiMjAyNC0xMi0wNyIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUlMwIiwicGFpZFVwVG8iOiIyMDI0LTEyLTA3IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJETSIsInBhaWRVcFRvIjoiMjAyNC0xMi0wNyIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiQ0wiLCJwYWlkVXBUbyI6IjIwMjQtMTItMDciLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IlJTVSIsInBhaWRVcFRvIjoiMjAyNC0xMi0wNyIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUlNDIiwicGFpZFVwVG8iOiIyMDI0LTEyLTA3IiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBDIiwicGFpZFVwVG8iOiIyMDI0LTEyLTA3IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJEUyIsInBhaWRVcFRvIjoiMjAyNC0xMi0wNyIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUkQiLCJwYWlkVXBUbyI6IjIwMjQtMTItMDciLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IlJDIiwicGFpZFVwVG8iOiIyMDI0LTEyLTA3IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJSU0YiLCJwYWlkVXBUbyI6IjIwMjQtMTItMDciLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUk0iLCJwYWlkVXBUbyI6IjIwMjQtMTItMDciLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IklJIiwicGFpZFVwVG8iOiIyMDI0LTEyLTA3IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJEUE4iLCJwYWlkVXBUbyI6IjIwMjQtMTItMDciLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IkRCIiwicGFpZFVwVG8iOiIyMDI0LTEyLTA3IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJEQyIsInBhaWRVcFRvIjoiMjAyNC0xMi0wNyIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUFMiLCJwYWlkVXBUbyI6IjIwMjQtMTItMDciLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IlJTViIsInBhaWRVcFRvIjoiMjAyNC0xMi0wNyIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJXUyIsInBhaWRVcFRvIjoiMjAyNC0xMi0wNyIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUFNJIiwicGFpZFVwVG8iOiIyMDI0LTEyLTA3IiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBDV01QIiwicGFpZFVwVG8iOiIyMDI0LTEyLTA3IiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlJTIiwicGFpZFVwVG8iOiIyMDI0LTEyLTA3IiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IkRQIiwicGFpZFVwVG8iOiIyMDI0LTEyLTA3IiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBEQiIsInBhaWRVcFRvIjoiMjAyNC0xMi0wNyIsImV4dGVuZGVkIjp0cnVlfV0sIm1ldGFkYXRhIjoiMDEyMDIzMTIwOUxQQUEwMDEwMDkiLCJoYXNoIjoiNTI1MDgyODgvMjUxMjMyNjE6LTE1MDQzMDI5NDAiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwidHJpYWwiOmZhbHNlLCJhaUFsbG93ZWQiOnRydWV9-QKJUeHFkc+NaPWlEFZFGpoBJBYjehR5cGPezKK9BKHVnVaydzLV4YSAnILt8mz9twXw9lIh0k/HivsPKrffP8F9gZkWA/rfjieSI0jziDr9WBARPzYKRlQHSw/iZn5VUn6zIR9U7uJC6Kd/jiaeLumn+dzL/ia9B/1dBUIg5WQlIOtld4xx2xR0gb4JCNBd4kQMV4SAC3Og13/APGkDiP7KzDz7T3DxmpSKvjAfG1Hg1jn2pt5B/3gmhOK5lmJKbGBDRW40f4sqyDpzXsA5DaPAAaFT07GSL5FlfdKfngGjcQdwQ18k1iFET6wSwWkk+p+OySDqegpFw3wx2Kzj+Ow==-MIIETDCCAjSgAwIBAgIBDzANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIyMTAxMDE2MDU0NFoXDTI0MTAxMTE2MDU0NFowHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMjEwMTAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/W3uCpU5M2y48rUR/3fFR6y4xj1nOm3rIuGp2brELVGzdgK2BezjnDXpAxVDw5657hBkAUMoyByiDs2MgmVi9IcqdAwpk988/Daaajq9xuU1of59jH9eQ9c3BmsEtdA4boN3VpenYKATwmpKYkJKVc07ZKoXL6kSyZuF7Jq7HoQZcclChbF75QJPGbri3cw9vDk/e46kuzfwpGftvl6+vKibpInO6Dv0ocwImDbOutyZC7E+BwpEm1TJZW4XovMBegHhWC04cJvpH1u98xoR94ichw0jKhdppywARe43rGU96163RckIuFmFDQKZV9SMUrwpQFu4Z2D5yTNqnlLRfAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQU5FZqQ4gnVc+inIeZF+o3ID+VhcEwSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBANLG1anEKid4W87vQkqWaQTkRtFKJ2GFtBeMhvLhIyM6Cg3FdQnMZr0qr9mlV0w289pf/+M14J7S7SgsfwxMJvFbw9gZlwHvhBl24N349GuthshGO9P9eKmNPgyTJzTtw6FedXrrHV99nC7spaY84e+DqfHGYOzMJDrg8xHDYLLHk5Q2z5TlrztXMbtLhjPKrc2+ZajFFshgE5eowfkutSYxeX8uA5czFNT1ZxmDwX1KIelbqhh6XkMQFJui8v8Eo396/sN3RAQSfvBd7Syhch2vlaMP4FAB11AlMKO2x/1hoKiHBU3oU3OKRTfoUTfy1uH3T+t03k1Qkr0dqgHLxiv6QU5WrarR9tx/dapqbsSmrYapmJ7S5+ghc4FTWxXJB1cjJRh3X+gwJIHjOVW+5ZVqXTG2s2Jwi2daDt6XYeigxgL2SlQpeL5kvXNCcuSJurJVcRZFYUkzVv85XfDauqGxYqaehPcK2TzmcXOUWPfxQxLJd2TrqSiO+mseqqkNTb3ZDiYS/ZqdQoGYIUwJqXo+EDgqlmuWUhkWwCkyo4rtTZeAj+nP00v3n8JmXtO30Fip+lxpfsVR3tO1hk4Vi2kmVjXyRkW2G7D7WAVt+91ahFoSeRWlKyb4KcvGvwUaa43fWLem2hyI4di2pZdr3fcYJ3xvL5ejL3m14bKsfoOv
2、Clikt
关于命令行工具的开源库,一键集成,具体的可以去全球最大XX交流社区看看。我们使用Gradle集成很简单
implementation 'com.github.ajalt.clikt:clikt:3.5.2'
3、代码编写
1、命令行实现
class Main(): CliktCommand(help = "这是图片压缩工具") {
private val currentPath: String by option("--current_path", "-c", help = "Current Path").prompt("请输入需要压缩的目录")
private val scale: Float by option("--scale", "-s", help = "scale是可以指定图片的大小,值在0到1之间,1f就是原图大小,0.5就是原图的一半大小,这里的大小是指图片的长宽 ").float()
.prompt("scaleSize")
private val outputQuality: Float by option("--quality", "-q", help = "outputQuality是图片的质量,值也是在0到1,越接近于1质量越好,越接近于0质量越差 ").float()
.prompt("outputQuality")
override fun run() {
println("压缩开始!")
try {
ThumbUtils.init(Paths.get(currentPath), scale, outputQuality).thumb()
}catch (e:Throwable){
println("压缩失败! ${e.printStackTrace()}")
}finally {
println("压缩结束!")
}
}
}
fun main(args: Array<String>) = Main().main(args)
我们创建一个Main类,继承CliktCommand()这个类,(来自clikt),这里面已经封装好命令行的实现了,我们只需要依葫芦画瓢,创建我们需要的入参即可:
private val currentPath: String by option("--current_path", "-c", help = "Current Path").prompt("请输入需要压缩的目录")
这个大家都看得懂,就不赘述了,具体的实现是在run函数中。Java入口是main函数,我们就建一个main函数,然后调用Main类。
2、图片压缩代码
关于图片压缩,不得不提到一个类,即BufferedImage:
BufferedImage 子类描述具有可访问图像数据缓冲区的 Image。BufferedImage 由图像数据的 ColorModel 和 Raster 组成。Raster 的 SampleModel 中 band 的数量和类型必须与 ColorModel 所要求的数量和类型相匹配,以表示其颜色和 alpha 分量。所有 BufferedImage 对象的左上角坐标都为 (0, 0)。因此,用来构造 BufferedImage 的任何 Raster 都必须满足:minX=0 且 minY=0。
此类依靠 Raster 的数据获取方法、数据设置方法,以及 ColorModel 的颜色特征化方法。
同时,我们知道图片的类型有很多,不同类型占用的大小是有区别的,比如TYPE_INT_ARGB, 表示一个图像,它具有合成整数像素的 8 位 RGBA 颜色分量,RGB表三原色,A代表透明度,TYPE_INT_RGB没有透明度的图像,占用肯定比TYPE_INT_ARGB要小,关于更多的类型查看这里。
实现压缩,我们可以通过compressionQuality,修改大小,达到压缩的目的,不过这种比较直接,如果是带有透明的效果的png格式,可能不太友好,可能图片会黑背景。
private fun compressImage(input: File, output: File, format: String, quality: Float) {
val bufferedImage = ImageIO.read(input)
val compressedImage = BufferedImage(bufferedImage.width, bufferedImage.height, BufferedImage.TYPE_INT_RGB)
// 将原始图像绘制到压缩图像,并填充背景色
val graphics = compressedImage.createGraphics()
graphics.color = Color.WHITE // 设置背景色
graphics.fillRect(0, 0, compressedImage.width, compressedImage.height)
graphics.drawImage(bufferedImage, 0, 0, null)
graphics.dispose()
// 设置压缩参数
val writer = ImageIO.getImageWritersByFormatName(format).next()
val writeParam = writer.defaultWriteParam
if (writeParam.canWriteCompressed()) {
writeParam.compressionMode = javax.imageio.ImageWriteParam.MODE_EXPLICIT
writeParam.compressionQuality = quality
}
// 写入压缩后的图像
ImageIO.createImageOutputStream(output).use { outputStream ->
writer.output = outputStream
writer.write(null, javax.imageio.IIOImage(compressedImage, null, null), writeParam)
}
}
关于压缩,谷歌也有开源库thumbnailator, 使用也简单
Thumbnails.of(inputFile)
.scale(scale.toDouble())
.outputQuality(outputQuality)
.toFile(outputFile)
不过同理,对于png格式的不友好,这里推荐另外一个库OpenViewerFX , 当然这个不只是用于压缩图片的:
// 导库
implementation 'org.jpedal:OpenViewerFX:6.6.14'
//使用
PngCompressor.compress(inputFile, inputFile)
这里使用的是6.6.14版本,高版本可能没有PngCompressor这个类了,PngCompressor很方面的是inputFile和outputFile可以是一个,这样可以直接覆盖。
完成上述流程之后,基本功能也就是实现了,只是需要做一下串联,如下代码:
import com.idrsolutions.image.png.PngCompressor
import net.coobird.thumbnailator.Thumbnails
import java.awt.Color
import java.awt.image.BufferedImage
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import javax.imageio.ImageIO
import kotlin.io.path.pathString
import kotlin.streams.toList
class ThumbUtils {
private val pathTypes = arrayOf(".jpg", ".jpeg", ".png")
lateinit var currentPath: Path
var scale: Float = 1.0f
var outputQuality: Float = 0.5f
fun thumb() {
val paths = listAllFiles(currentPath)
if (paths.isEmpty()) {
println("没有需要压缩的文件")
return
}
val cacheFile = File(currentPath.parent.pathString + "/cache")
if (!cacheFile.exists()) {
cacheFile.mkdirs()
}
paths.forEach { path ->
println("准备压缩 ${path}")
pathTypes.forEach { type ->
if (path.pathString.endsWith(type)) {
println("压缩中 ${path}")
val inputFile = path.toFile()
if (type === ".png") {
PngCompressor.compress(inputFile, inputFile)
} else {
val outputFile = File(cacheFile.path, "compressed_${inputFile.name}")
if (!outputFile.exists()) {
outputFile.createNewFile()
}
Thumbnails.of(inputFile)
.scale(scale.toDouble())
.outputQuality(outputQuality)
.toFile(outputFile)
// compressImage(inputFile, outputFile, type, outputQuality)
// 覆盖原有图片
Files.move(outputFile.toPath(), inputFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
}
}
}
if (cacheFile.exists()) {
cacheFile.delete()
}
}
private fun compressImage(input: File, output: File, format: String, quality: Float) {
val bufferedImage = ImageIO.read(input)
val compressedImage = BufferedImage(bufferedImage.width, bufferedImage.height, BufferedImage.TYPE_INT_RGB)
// 将原始图像绘制到压缩图像,并填充背景色
val graphics = compressedImage.createGraphics()
graphics.color = Color.WHITE // 设置背景色
graphics.fillRect(0, 0, compressedImage.width, compressedImage.height)
graphics.drawImage(bufferedImage, 0, 0, null)
graphics.dispose()
// 设置压缩参数
val writer = ImageIO.getImageWritersByFormatName(format).next()
val writeParam = writer.defaultWriteParam
if (writeParam.canWriteCompressed()) {
writeParam.compressionMode = javax.imageio.ImageWriteParam.MODE_EXPLICIT
writeParam.compressionQuality = quality
}
// 写入压缩后的图像
ImageIO.createImageOutputStream(output).use { outputStream ->
writer.output = outputStream
writer.write(null, javax.imageio.IIOImage(compressedImage, null, null), writeParam)
}
}
private fun listAllFiles(directory: Path): List<Path> {
return if (Files.isDirectory(directory)) {
Files.walk(directory)
.filter { Files.isRegularFile(it) } // 只过滤出文件
.toList() // 将 Stream 转换为 List
} else {
emptyList() // 如果传入的不是一个目录,则返回空列表
}
}
companion object {
fun init(
currentPath: Path,
scale: Float,
outputQuality: Float
): ThumbUtils {
val thumbUtils = ThumbUtils()
thumbUtils.currentPath = currentPath
thumbUtils.scale = scale
thumbUtils.outputQuality = outputQuality
return thumbUtils
}
}
}
流程就是根据输入的路径,读取路径里面的图片,生成列表,然后逐个去压缩。同时生成临时目录,存放临时文件,覆盖之后,再把临时目录删除。
运行man函数:
成功了
3、生成可执行文件
上面是在工程中运行的,不方便,我们需要将其打成可执行文件。
jar
java最终是可以达成jar的,我们只需要在build.gradle中加入配置即可:
jar {
exclude("**/module-info.class")
manifest {
attributes 'Main-Class': 'MainKt'
}
from {
configurations.runtimeClasspath.collect {
it.isDirectory() ? it : zipTree(it)
}
}
}
如果报错,is a duplicate but no duplicate handling strategy has been set. 可以加上:
// 生成的分发包中存在重复的文件
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
如果报错: Invalid signature file digest for Manifest main attributes 以排除签名文件
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
执行这个jar,就可以生成对应的jar文件了,在命令行输入 java -jar xxx.jar -xx 即可执行了
exec/bat
此步骤依赖上面的jar,jar包执行前依赖java -jar 要解码一次,还不符合我们的要求,所以我们这里要使用distZip/distTar来生成我们需要的可执行文件。执行gradle packageDistribution, 即可生成对应的可执行文件:
解压后,我们再讲环境变量配置在.bash_profile中,
打开命令行工具,输入命令:
成功!
四、写在最后
本文以最简单的方式实现了一个CLI的实现,功能很简单。通过这个入门,学习一下CLI的实现。 示例代码:在这里