I remember that when I wrote the project in 2017, I used Luban as the image compression tool. After many years, I still found Luban in the two recent projects, so I think it is necessary to record the old friend.
The best way to learn a new technology is to read the official documentation (Github - Luban) , because in addition to introducing the basic usage, you can also learn the source code if you are interested; and I record blog more A process of my learning and reflection, I hope it can help you
basic understanding
Compared with the Ios market, the Android market is more open and extensive; there are also many manufacturers in China, which means that there are more mobile phone brands, which means that the price of each mobile phone 拍照分辨率,屏幕显示分辨率
may question;
With 拍照分辨率
the improvement of mobile phones, image compression is a very important issue; if it is only 单纯对图片进行裁切,压缩可能很难满足诉求
(about the original compression method and principle, I will add an article later when I have time), it is because 没有一个标准
, if 裁切过头图片太小,质量压缩过头则显示效果太差
.
个人见解:所谓的标准,可以说是策略,指的是不同场景下采用不同的方式
(If you don't understand, you can look at the strategy mode and open your mind)
In some project actual combat scenarios, it can be seen图片压缩的重要性
, for example~
加载大图
wasting too much traffic while still可能发生OOM
(for now)上传大图
wasting too much traffic while still可能出现上传失败的结果
compression effect
So this senior thought about how the App giant "WeChat" would deal with it Luban(鲁班)就是通过在微信朋友圈发送近100张不同分辨率图片,对比原图与微信压缩后的图片逆向推算出来的压缩算法
.
The same picture has passed through Luban压缩
and WeChat压缩
de效果与对比
(the compression effect is very good, so it is still used until now)
compression algorithm
作者考虑到其他语言也想要实现Luban,所以描述了一遍
Algorithm steps
3rd gear compression ( 参考最新版微信压缩效果
)
作者在2018、2019年后就没再维护了,有俩种可能一种就是Luban已经很稳定了,另一种就是开启人生新篇章了吧
1. 判断图片比例值
, whether it is in the following range;
- [1, 0.5625) means that the picture is in the ratio range of [1:1 ~ 9:16)
- [0.5625, 0.5) means that the picture is in the ratio range of [9:16 ~ 1:2)
- [0.5, 0) means that the picture is in the ratio range of [1:2 ~ 1:∞)
2.判断图片最长边是否过边界值
;
- [1, 0.5625) boundary values: 1664 * n (n=1), 4990 * n (n=2), 1280 * pow(2, n-1) (n≥3)
- [0.5625, 0.5) The boundary value is: 1280 * pow(2, n-1) (n≥1)
- [0.5, 0) The boundary value is: 1280 * pow(2, n-1) (n≥1)
3. 计算压缩图片实际边长值
, based on the calculation result of step 2, if it exceeds a certain boundary value: width / pow(2, n-1), height/pow(2, n-1) 4. , based on the results of steps 2
and 计算压缩图片的实际文件大小
3 The larger the image ratio, the larger the file size.
size = (newW * newH) / (width * height) * m;
- [1, 0.5625) then width & height corresponds to 1664, 4990, 1280 * n (n≥3), m corresponds to 150, 300, 300;
- [0.5625, 0.5) then width = 1440, height = 2560, m = 200;
- [0.5, 0) then width = 1280, height = 1280 / scale, m = 500; Note: scale is a proportional value
5.判断第4步的size是否过小
- [1, 0.5625) then the minimum size corresponds to 60, 60, 100
- [0.5625, 0.5) then the minimum size is 100
- [0.5, 0) then the minimum size is 100
6.将前面求到的值压缩图片 width, height, size 传入压缩流程,压缩图片直到满足以上数值
method list
method | describe |
---|---|
load | Import the original image |
filter | Set the conditions for enabling compression |
ignoreBy | Uncompressed threshold, in K |
setFocusAlpha | Set whether to keep the transparent channel |
setTargetDir | Cache compressed image path |
setCompressListener | Compression callback interface |
setRenameListener | Rename interface before minification |
basic use
The following usage methods can be seen in Luban , yes
作者提供的一些调用示例
, it is convenient for everyone to use quickly
build.gradle
Introduce dependencies
implementation 'top.zibin:Luban:1.1.8'
asynchronous call
Luban内部采用IO线程进行图片压缩
, the external call only needs to set up the result monitoring:
Luban.with(this)
.load(photos)
.ignoreBy(100)
.setTargetDir(getPath())
.filter(new CompressionPredicate() {
@Override
public boolean apply(String path) {
return !(TextUtils.isEmpty(path) || path.toLowerCase().endsWith(".gif"));
}
})
.setCompressListener(new OnCompressListener() {
@Override
public void onStart() {
// TODO 压缩开始前调用,可以在方法内启动 loading UI
}
@Override
public void onSuccess(File file) {
// TODO 压缩成功后调用,返回压缩后的图片文件
}
@Override
public void onError(Throwable e) {
// TODO 当压缩过程出现问题时调用
}
}).launch();
synchronous call
同步方法请尽量避免在主线程调用以免阻塞主线程
, the following is rxJava
an example of calling ( 应该也支持用协程写,只要将压缩操作放到子线程即可
)
Flowable.just(photos)
.observeOn(Schedulers.io())
.map(new Function<List<String>, List<File>>() {
@Override public List<File> apply(@NonNull List<String> list) throws Exception {
// 同步方法直接返回压缩后的文件
return Luban.with(MainActivity.this).load(list).get();
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
Project combat
以下均为我在项目中使用Luban的一些伪代码,可供借鉴
ImageCompressor
package com.jsmedia.jsmanager.utils.imgUtils
import android.content.Context
import top.zibin.luban.Luban
import top.zibin.luban.OnCompressListener
import java.io.File
object ImageCompressor {
private const val IGNORE_SIZE = 1
fun compress(context: Context, imageFile: File, compressResult: CompressResult) {
Luban.with(context)
.load(imageFile)
.ignoreBy(IGNORE_SIZE)
.setCompressListener(object : OnCompressListener {
override fun onStart() {
}
override fun onSuccess(file: File) {
compressResult.onSuccess(file)
}
override fun onError(e: Throwable) {
compressResult.onError(e)
}
}).launch()
}
interface CompressResult {
fun onSuccess(file: File)
fun onError(e: Throwable)
}
}
calling method
ImageCompressor.compress(
this@StorePhotoActivity,
File(filePathByUri),
object : ImageCompressor.CompressResult {
override fun onSuccess(file: File) {
}
override fun onError(e: Throwable) {
}
})
Project - Pseudocode
/**
* 拍照
* */
fun onCamera(view: View?) {
ImagePicker.getInstance().startCamera(this, false, object : PickCallback() {
override fun onPermissionDenied(permissions: Array<String?>?, message: String?) {
ToastUtils.showShort(message)
}
override fun cropConfig(builder: ActivityBuilder) {
builder.setMultiTouchEnabled(true)
.setGuidelines(CropImageView.Guidelines.ON_TOUCH)
.setCropShape(CropImageView.CropShape.OVAL)
.setRequestedSize(400, 400)
.setFixAspectRatio(true)
.setAspectRatio(1, 1)
}
override fun onPickImage(imageUri: Uri?) {
super.onPickImage(imageUri)
Log.e("tag", imageUri.toString())
var bitmap = BitmapFactory.decodeStream(imageUri?.let {
contentResolver.openInputStream(
it
)
});
var filePathByUri = FileUtil.getFilePathByUri(this@StorePhotoActivity, imageUri)
ImageCompressor.compress(
this@StorePhotoActivity,
File(filePathByUri),
object : ImageCompressor.CompressResult {
override fun onSuccess(file: File) {
mViewModel.upload(
2,
"album",
file.absolutePath,
MMKV.defaultMMKV().decodeString("storeId").toString()
)
}
override fun onError(e: Throwable) {
}
})
}
override fun onCropImage(imageUri: Uri?) {
}
})
}
/**
* 相册
* */
fun onGallery(view: View?) {
ImagePicker.getInstance().startGallery(this, false, object : PickCallback() {
override fun onPermissionDenied(permissions: Array<String?>?, message: String?) {
ToastUtils.showShort(message)
}
override fun onPickImage(imageUri: Uri?) {
Log.e("tag", imageUri.toString())
var bitmap = BitmapFactory.decodeStream(imageUri?.let {
contentResolver.openInputStream(
it
)
});
var filePathByUri = FileUtil.getFilePathByUri(this@StorePhotoActivity, imageUri)
ImageCompressor.compress(
this@StorePhotoActivity,
File(filePathByUri),
object : ImageCompressor.CompressResult {
override fun onSuccess(file: File) {
mViewModel.upload(
2,
"album",
file.absolutePath,
MMKV.defaultMMKV().decodeString("storeId").toString()
)
}
override fun onError(e: Throwable) {
}
})
}
})
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
ImagePicker.getInstance()
.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
ImagePicker.getInstance().onActivityResult(this, requestCode, resultCode, data)
}