「iOS」浅谈图片压缩

Posted by Weixi on 2024-03-07

一、前言

图片基本上是目前移动端无法逃避的内容 最基础的用户头像、相册、评论都有涉及

涉及到了前端与后端的交互 那么一定会考虑到图片压缩

本篇文章就是作者近期涉及到图片压缩所踩到的一些坑

二、一些基础知识

所谓大小

图片分为两个大小 分辨率大小文件大小

通常文件大小与分辨率大小是有正相关关系的

分辨率高 * 分辨率宽 = 像素点数量

像素点的格式化方式、文件格式、压缩率 等等影响到 文件大小

而图片压缩的最终目的 就是 获取更小的图片文件大小

现实与里世界

通常我们看到的某张图片是JPG(JPEG)/PNG/WEBP格式存在的

但他其实只是一种文件格式

在镜子后面与内存打交道的是图片的数据流 Android-BitMap iOS-NSData 其他大部分是 [Bit]

通常通过选择图片的路径后会得到一串图片的数据流 之后再去转换成某些图片的格式 呈现在手机上

在转换图片的期间可以选择的压缩率 但通常会造成不可逆的后果 也就是无法再将某些图片转回数据流

三、压缩方法

通常简单压缩流程分为两个部分 设置图像分辨率设置压缩质量

大部分语言也都支持这个内容 但是寻找这两个数值甜点位置 是解决图片压缩的重点

在此笔者介绍两个用到感觉还不错的方法

二分法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
+ (NSData *)imageDataWithLimitByteSize:(NSUInteger)maxLength image:(UIImage *)image {
//首先判断原图大小是否在要求内,如果满足要求则不进行压缩
CGFloat compression = 1;
NSData *data = UIImageJPEGRepresentation(image, compression);
if (data.length < maxLength) return data;
//原图大小超过范围,先进行“压处理”,这里 压缩比 采用二分法进行处理,6次二分后的最小压缩比是0.015625,已经够小了
CGFloat max = 1;
CGFloat min = 0;
for (int i = 0; i < 6; ++i) {
compression = (max + min) / 2;
data = UIImageJPEGRepresentation(image, compression);
if (data.length < maxLength * 0.9) {
min = compression;
} else if (data.length > maxLength) {
max = compression;
} else {
break;
}
}
//判断“压处理”的结果是否符合要求,符合要求就
UIImage *resultImage = [UIImage imageWithData:data];
if (data.length < maxLength) return data;

//缩处理,直接用大小的比例作为缩处理的比例进行处理,因为有取整处理,所以一般是需要两次处理
NSUInteger lastDataLength = 0;
while (data.length > maxLength && data.length != lastDataLength) {
lastDataLength = data.length;
//获取处理后的尺寸
CGFloat ratio = (CGFloat)maxLength / data.length;
CGSize size = CGSizeMake((NSUInteger)(resultImage.size.width * sqrtf(ratio)),
(NSUInteger)(resultImage.size.height * sqrtf(ratio)));
//通过图片上下文进行处理图片
UIGraphicsBeginImageContext(size);
[resultImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//获取处理后图片的大小
data = UIImageJPEGRepresentation(resultImage, compression);
}

return data;
}

大致思路就是通过指定压缩后的文件大小确认压缩次数

主要缺点就是没有动态压缩 对某些upload有硬性图片大小的要求的可以使用

笔者遇到过一个其他问题 就是通过这个方法压缩出的是NSData 有时上传依然需要使用UIImage 然后再通过转换一层后就又超过了大小范围 但这是UIKit框架问题 在这里不再深入赘述

luban

安卓圈曾诞生过一个被誉为接近微信朋友圈压缩算法的工具

作者称其为luban 是其在发送100张图片后对微信算法的总结与归纳 发表了一个第三方库

主要源码也很简单 这里也对其进行简单分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private int computeSize() {
srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;

int longSide = Math.max(srcWidth, srcHeight);
int shortSide = Math.min(srcWidth, srcHeight);

float scale = ((float) shortSide / longSide);
if (scale <= 1 && scale > 0.5625) {
if (longSide < 1664) {
return 1;
} else if (longSide < 4990) {
return 2;
} else if (longSide > 4990 && longSide < 10240) {
return 4;
} else {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
}
} else if (scale <= 0.5625 && scale > 0.5) {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
} else {
return (int) Math.ceil(longSide / (1280.0 / scale));
}
}

private Bitmap rotatingImage(Bitmap bitmap, int angle) {
Matrix matrix = new Matrix();

matrix.postRotate(angle);

return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}

File compress() throws IOException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = computeSize();

Bitmap tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
ByteArrayOutputStream stream = new ByteArrayOutputStream();

if (Checker.SINGLE.isJPG(srcImg.open())) {
tagBitmap = rotatingImage(tagBitmap, Checker.SINGLE.getOrientation(srcImg.open()));
}
tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 60, stream);
tagBitmap.recycle();

FileOutputStream fos = new FileOutputStream(tagImg);
fos.write(stream.toByteArray());
fos.flush();
fos.close();
stream.close();

return tagImg;
}
}

源码优化

金无足赤人无完人 仔细审视这段代码其实还能发现一些问题

1
2
3
4
5
else {
// return longSide / 1280 == 0 ? 1 : longSide / 1280;
// 代码之前已经判断了 longSide < 1664 不会再出现 < 1280 的情况了
return longSide / 1280;
}
1
2
3
4
5
6
else {
// return (int) Math.ceil(longSide / (1280.0 / scale));
// 由于上面已经对scale进行计算 最后的返回可以优化一下公式
// float scale = ((float) shortSide / longSide);
return (int) Math.ceil(shortSide / 1280.0)
}

以及目前的luban对JPG的压缩率是固定确认的60% 压缩率其实可以再做调整

比如根据修改尺寸前的图片大小与修改尺寸后的图片大小进行比较

在动态的设定压缩率也是不错的选择

唏嘘

无论代码如何 作者只是对发送了100次图片后的逆向破解就得到了次算法 且无私开源 实属不易

转眼24开年WXG的年终开奖 高达20个月 微信在资本的推动下还在不停的飞速发展

当年号称能够媲美微信朋友圈图片压缩算法的luban 如今五、六年后是否还能一战?

却得知作者同生活对线去了 github库已多年没有更新

昙花一现的奇迹 V.S. 柴米油盐的心酸

只能让人感到无尽的唏嘘

融合算法

没办法 笔者也需要与生活对线 在对比压缩效果之后 编写了一套Unity的Texutre压缩算法

目前自测效果不错 也是对上述内容的一个总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private static int EnsureEven(int size)
{
return size % 2 == 1 ? size + 1 : size;
}

private static int ComputeCompressSize(int width, int height)
{
var originalWidth = EnsureEven(width);
var originalHeight = EnsureEven(height);

var longSide = Math.Max(originalWidth, originalHeight);
var shortSide = Math.Min(originalWidth, originalHeight);

var aspectRatio = (double) shortSide / (double) longSide;

return aspectRatio switch
{
<= 1 and > 0.5625 => longSide switch
{
< 1664 => 1,
< 4990 => 2,
> 4990 and < 10240 => 4,
_ => longSide / 1280,
},
<= 0.5625 and > 0.5 => longSide <= 1280 ? 1 : longSide / 1280,
_ => (int) Mathf.Ceil((float) (longSide / 1280.0)) // 在笔者的环境下 不需要保留长图的尺寸 故选择压缩程度更大的结果
};
}

public static Texture2D CompressTexture(Texture2D sourceTexture)
{
var size = ComputeCompressSize(sourceTexture.width, sourceTexture.height);
var aspectRatio = Math.Pow(0.9f, size);
var targetWidth = (int) (sourceTexture.width * aspectRatio);
var targetHeight = (int) (sourceTexture.height * aspectRatio);
return ResizeTexture(sourceTexture, targetWidth, targetHeight);
}

四、尾语

不追求极限、不涉及底层 这些简单的算法到现在也能打

这篇文章是这几天我对成果的一个总结

也希望能使后人的路走得再轻松一点

祝好

参考文章

Curzibn/Luban: Luban(鲁班)—Image compression with efficiency very close to WeChat Moments/可能是最接近微信朋友圈的图片压缩算法 (github.com)

可能是最详细的Android图片压缩原理分析(二)—— 鲁班压缩算法解析 - 掘金 (juejin.cn)

GuoZhiQiang/Luban_iOS: Wiki (github.com)

Luban压缩实现分析 | Mycroft