flutter图片压缩库对比
Posted 一叶飘舟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了flutter图片压缩库对比相关的知识,希望对你有一定的参考价值。
flutter_luban和flutter_image_compress
最近在做flutter的项目的时候用到了图片上传和图片压缩,开始使用的压缩库是flutter_luban,压缩的效果不错,但是在一些比较老的手机上面压缩的效率很慢,一个5、6M的图片压缩需要大概30秒的时间,后来获取图片的时候先把图片的质量缩小了,然后再压缩,压缩的时长并没有改变。再后来只能先换了一个三方库flutter_image_compress先解决问题,然后有时间研究了下之前的库压缩慢的问题进行对比。
flutter_luban压缩介绍
flutter_luban方法调用起来也比较简单,先将需要压缩的图片封装到CompressObject类中,然后调用Luban.compressImage既可。
CompressObject compressObject = CompressObject(
imageFile: image,
path: path
);
Luban.compressImage(compressObject).then((_path)
// _path为压缩后图片路径
File newImage = new File(_path);
);
然后来看下compressImage方法的具体实现
static Future<String> compressImage(CompressObject object) async
return compute(_lubanCompress, object);
然后就是调用到了_lubanCompress这个方法。从这个方法可以看出,flutter_luban只支持jpg和png格式的图片压缩,然后基本的步骤为
- 图片的宽高进行的一系列的计算和校验
- 然后根据不同的请款进行不同的压缩方式,其中例如宽高比例在0.5626到1之间,高度小于1664,图片大小小于150K,则按照传入的压缩比例进行图片质量压缩。
- 如果不需要直接进行质量压缩的,会根据传入的压缩模式进行压缩
- 没有传入压缩模式的,则根据图片大小,大于500K则进行_small2LargeCompressImage,小于500K则进行_large2SmallCompressImage
static String _lubanCompress(CompressObject object)
// 首先是将文件类型转换为图片类型
// 这个转换过程大概耗费了10S左右,因为需要大量的计算
Image image = decodeImage(object.imageFile.readAsBytesSync());
var length = object.imageFile.lengthSync();
print(object.imageFile.path);
bool isLandscape = false;
// 这里可以看出该算法只支持jpg和png的图片类型
const List<String> jpgSuffix = ["jpg", "jpeg", "JPG", "JPEG"];
const List<String> pngSuffix = ["png", "PNG"];
// 判断图片类型
bool isJpg = _parseType(object.imageFile.path, jpgSuffix);
bool isPng = false;
if (!isJpg) isPng = _parseType(object.imageFile.path, pngSuffix);
// 获取图片大小,保持高>宽的方向,计算宽高比
double size;
int fixelW = image.width;
int fixelH = image.height;
double thumbW = (fixelW % 2 == 1 ? fixelW + 1 : fixelW).toDouble();
double thumbH = (fixelH % 2 == 1 ? fixelH + 1 : fixelH).toDouble();
double scale = 0;
if (fixelW > fixelH)
scale = fixelH / fixelW;
var tempFixelH = fixelW;
var tempFixelW = fixelH;
fixelH = tempFixelH;
fixelW = tempFixelW;
isLandscape = true;
else
scale = fixelW / fixelH;
// 存储文件
var decodedImageFile;
if (isJpg)
decodedImageFile = new File(
object.path + '/img_$DateTime.now().millisecondsSinceEpoch.jpg');
else if (isPng)
decodedImageFile = new File(
object.path + '/img_$DateTime.now().millisecondsSinceEpoch.png');
else
throw Exception("flutter_luban don't support this image type");
if (decodedImageFile.existsSync())
decodedImageFile.deleteSync();
// 对于图片的长款进行一系列的计算
var imageSize = length / 1024;
if (scale <= 1 && scale > 0.5625)
if (fixelH < 1664)
if (imageSize < 150)
// 如果宽高比例在0.5626到1之间,高度小于1664,图片大小小于150K,则按照传入的压缩比例进行图片质量压缩
decodedImageFile
.writeAsBytesSync(encodeJpg(image, quality: object.quality));
return decodedImageFile.path;
size = (fixelW * fixelH) / pow(1664, 2) * 150;
size = size < 60 ? 60 : size;
else if (fixelH >= 1664 && fixelH < 4990)
thumbW = fixelW / 2;
thumbH = fixelH / 2;
size = (thumbH * thumbW) / pow(2495, 2) * 300;
size = size < 60 ? 60 : size;
else if (fixelH >= 4990 && fixelH < 10240)
thumbW = fixelW / 4;
thumbH = fixelH / 4;
size = (thumbW * thumbH) / pow(2560, 2) * 300;
size = size < 100 ? 100 : size;
else
int multiple = fixelH / 1280 == 0 ? 1 : fixelH ~/ 1280;
thumbW = fixelW / multiple;
thumbH = fixelH / multiple;
size = (thumbW * thumbH) / pow(2560, 2) * 300;
size = size < 100 ? 100 : size;
else if (scale <= 0.5625 && scale >= 0.5)
if (fixelH < 1280 && imageSize < 200)
decodedImageFile
.writeAsBytesSync(encodeJpg(image, quality: object.quality));
return decodedImageFile.path;
int multiple = fixelH / 1280 == 0 ? 1 : fixelH ~/ 1280;
thumbW = fixelW / multiple;
thumbH = fixelH / multiple;
size = (thumbW * thumbH) / (1440.0 * 2560.0) * 200;
size = size < 100 ? 100 : size;
else
int multiple = (fixelH / (1280.0 / scale)).ceil();
thumbW = fixelW / multiple;
thumbH = fixelH / multiple;
size = ((thumbW * thumbH) / (1280.0 * (1280 / scale))) * 500;
size = size < 100 ? 100 : size;
if (imageSize < size)
decodedImageFile
.writeAsBytesSync(encodeJpg(image, quality: object.quality));
return decodedImageFile.path;
Image smallerImage;
if (isLandscape)
smallerImage = copyResize(image,
width: thumbH.toInt(),
height: object.autoRatio ? null : thumbW.toInt());
else
smallerImage = copyResize(image,
width: thumbW.toInt(),
height: object.autoRatio ? null : thumbH.toInt());
if (decodedImageFile.existsSync())
decodedImageFile.deleteSync();
// 根据传入的压缩模式进行压缩
if (object.mode == CompressMode.LARGE2SMALL)
_large2SmallCompressImage(
image: smallerImage,
file: decodedImageFile,
quality: object.quality,
targetSize: size,
step: object.step,
isJpg: isJpg,
);
else if (object.mode == CompressMode.SMALL2LARGE)
_small2LargeCompressImage(
image: smallerImage,
file: decodedImageFile,
quality: object.step,
targetSize: size,
step: object.step,
isJpg: isJpg,
);
else
// 没有传压缩模式,则判断图片大小是否大于500K
if (imageSize < 500)
_large2SmallCompressImage(
image: smallerImage,
file: decodedImageFile,
quality: object.quality,
targetSize: size,
step: object.step,
isJpg: isJpg,
);
else
// 本次进行测试的图片为4M左右所以调用了以下方法
_small2LargeCompressImage(
image: smallerImage,
file: decodedImageFile,
quality: object.step,
targetSize: size,
step: object.step,
isJpg: isJpg,
);
return decodedImageFile.path;
因为如今手机拍摄的照片都偏大,平均在3~6M左右,所以本次选取的是一个4M左右的图片,则简单看一下_small2LargeCompressImage方法。
static _small2LargeCompressImage(
Image image,
File file,
quality,
targetSize,
step,
bool isJpg: true,
)
// 根据图片类型进行处理
if (isJpg)
var im = encodeJpg(image, quality: quality);
var tempImageSize = Uint8List.fromList(im).lengthInBytes;
if (tempImageSize / 1024 < targetSize && quality <= 100)
quality += step;
// 这里可以看出使用了递归,如果压缩效果不够的话会进行多次的压缩,
// 所以之前手机压缩图片使用了30S,这里连续压缩了6次才达到想要的效果,耗费了大概12S
_small2LargeCompressImage(
image: image,
file: file,
quality: quality,
targetSize: targetSize,
step: step,
isJpg: isJpg,
);
return;
file.writeAsBytesSync(im);
else
_compressPng(
image: image,
file: file,
targetSize: targetSize,
large2Small: false,
);
所以flutter_luban就是根据图片的大小以及宽高的情况,对图片进行重复的质量压缩,直到达到预期的效果为止,如果是内存较小或者cpu运算能力差的手机,则会耗费很长时间。再有就是,该方法没有单独开线程进行压缩,然后我使用的时候也没有开线程,所以在主线程中导致更加慢。
flutter_image_compress压缩介绍
flutter_image_compress算法使用起来也比较简单,但是这个方法压缩之后返回的是一个二进制数组也就是byte串,如果需要文件格式还需要再转一下文件或者图片格式。
Future<List<int>> testCompressFile(File file) async
final result = await FlutterImageCompress.compressWithFile(
file.absolute.path,
minWidth: 2300,//压缩后的最小宽度
minHeight: 1500,//压缩后的最小高度
quality: 20,//压缩质量
rotate: 0,//旋转角度
);
return result;
await testCompressFile(image).then((value)
String newPath = image.path.replaceAll(".jpg", "1.jpg");
File file = new File(newPath);
//保存压缩后图片
file.writeAsBytes(value).then((newImage)
//newImage为压缩后图片
);
);
然后看下compressWithFile方法,这个方法的话首先是校验了下必传的参数,然后判断会否传了文件的路径,有路径则通过channel通道调用到android的代码中进行压缩的操作。
static Future<List<int>> compressWithFile(
String path,
int minWidth = 1920,
int minHeight = 1080,
int inSampleSize = 1,
int quality = 95,
int rotate = 0,
bool autoCorrectionAngle = true,
CompressFormat format = CompressFormat.jpeg,
bool keepExif = false,
) async
assert(
path != null,
"A non-null String must be provided to FlutterImageCompress.",
);
if (path == null || !File(path).existsSync())
return [];
final support = await _validator.checkSupportPlatform(format);
if (!support)
return null;
// 通过channel调用到Android代码
final result = await _channel.invokeMethod("compressWithFile", [
path,
minWidth,
minHeight,
quality,
rotate,
autoCorrectionAngle,
_convertTypeToInt(format),
keepExif,
inSampleSize,
]);
return convertDynamic(result);
然后就要去Android代码中找相应的调用方法了。首先找到接受这个调用的文件,因为包名是flutter_image_compress,所以在Android代码下会有一个flutter_image_compress的文件夹,存放的就是这个三方库所需要的代码,其中FlutterImageCompressPlugin中的onMethodCall方法就是作为接受flutter端的调用的方法。我们可以看到compressWithFile就在其中,就可以跟踪对应的代码进行分析了。
继续跟踪CompressFileHandler方法,首先就是对于参数的获取和校验以及图片格式的校验,然后开始压缩图片,进行handleFile操作。
class CompressFileHandler(private val call: MethodCall, result: MethodChannel.Result) : ResultHandler(result)
companion object
@JvmStatic
private val executor = Executors.newFixedThreadPool(5)
fun handle(registrar: PluginRegistry.Registrar)
executor.execute
@Suppress("UNCHECKED_CAST") val args: List<Any> = call.arguments as List<Any>
// 获取flutter端发送的参数
val filePath = args[0] as String
var minWidth = args[1] as Int
var minHeight = args[2] as Int
val quality = args[3] as Int
val rotate = args[4] as Int
val autoCorrectionAngle = args[5] as Boolean
val format = args[6] as Int
val keepExif = args[7] as Boolean
val inSampleSize = args[8] as Int
// 校验图片格式,现支持jpeg, png, heic, webp
val formatHandler = FormatRegister.findFormat(format)
if (formatHandler == null)
log("No support format.")
reply(null)
return@execute
// 是否自动校验角度
val exifRotate =
if (autoCorrectionAngle)
val bytes = File(filePath).readBytes()
Exif.getRotationDegrees(bytes)
else
0
if (exifRotate == 270 || exifRotate == 90)
val tmp = minWidth
minWidth = minHeight
minHeight = tmp
val targetRotate = rotate + exifRotate
try
val outputStream = ByteArrayOutputStream()
// 压缩图片
formatHandler.handleFile(registrar.context(), filePath, outputStream, minWidth, minHeight, quality, targetRotate, keepExif, inSampleSize)
reply(outputStream.toByteArray())
catch (e: Exception)
if (FlutterImageCompressPlugin.showLog) e.printStackTrace()
reply(null)
找到CommonHandler文件中的handleFile方法,其中bitmap.compress就是对图片进行压缩的方法
override fun handleFile(context: Context, path: String, outputStream: OutputStream, minWidth: Int, minHeight: Int, quality: Int, rotate: Int, keepExif: Boolean, inSampleSize: Int)
val options = BitmapFactory.Options()
options.inJustDecodeBounds = false
options.inPreferredConfig = Bitmap.Config.RGB_565
options.inSampleSize = inSampleSize
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M)
@Suppress("DEPRECATION")
options.inDither = true
val bitmap = BitmapFactory.decodeFile(path, options)
// 进行压缩
val array = bitmap.compress(minWidth, minHeight, quality, rotate, type)
if (keepExif && bitmapFormat == Bitmap.CompressFormat.JPEG)
val byteArrayOutputStream = ByteArrayOutputStream()
byteArrayOutputStream.write(array)
val tmpOutputStream = ExifKeeper(path).writeToOutputStream(
context,
byteArrayOutputStream
)
outputStream.write(tmpOutputStream.toByteArray())
else
outputStream.write(array)
在BitmapCompressExt.kt文件中定义了compress方法,根据传的最小高度和宽度确定出图片的最终大小,保证高度和宽度都不小于最小高度和宽度,然后根据计算后的高度和宽度,对位图进行压缩,一次既可。
fun Bitmap.compress(minWidth: Int, minHeight: Int, quality: Int, rotate: Int = 0, format: Int): ByteArray
val outputStream = ByteArrayOutputStream()
compress(minWidth, minHeight, quality, rotate, outputStream, format)
return outputStream.toByteArray()
fun Bitmap.compress(minWidth: Int, minHeight: Int, quality: Int, rotate: Int = 0, outputStream: OutputStream, format: Int = 0)
val w = this.width.toFloat()
val h = this.height.toFloat()
log("src width = $w")
log("src height = $h")
val scale = calcScale(minWidth, minHeight)
log("scale = $scale")
val destW = w / scale
val destH = h / scale
log("dst width = $destW")
log("dst height = $destH")
Bitmap.createScaledBitmap(this, destW.toInt(), destH.toInt(), true)
.rotate(rotate)
.compress(convertFormatIndexToFormat(format), quality, outputStream)
总结
整体来说,flutter_luban方法主要耗时在计算上面,而且全部是通过flutter来实现的,所以对于内存较低,CPU也低的机型来说,很耗时。但是对于flutter_image_compress来说,主要的计算方面都是在Android中实现的,并且开了单独的线程进行处理,计算量本来也不大,所以效率会高很多。
以上是关于flutter图片压缩库对比的主要内容,如果未能解决你的问题,请参考以下文章