UIImage 二分压缩图片

开发中,通常需要对图片进行压缩处理(多用于图片压缩上传),一般我们可能会直接使用系统提供的API将图片对象转为Data对象以实现需求:

1
2
3
4
5
Returns the data for the specified image in PNG format.
UIKIT_EXTERN NSData * UIImagePNGRepresentation(UIImage *image);

Returns the data for the specified image in JPEG format.
UIKIT_EXTERN NSData * UIImageJPEGRepresentation(UIImage *image, CGFloat compressionQuality);

由于UIImagePNGRepresentation()方法无法调整压缩质量,通常我们会使用UIImageJPEGRepresentation()方法。但是,若图片本身尺寸较大,而压缩质量较低,这样实现会出现图片点位像素不足,图片模糊不清。因而,我们通常会在图片压缩之前先调整图片的尺寸。同时,为方便代码管理,我们通过UIImage分类方法的形式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 根据指定尺寸,调整图片尺寸(图片可能会被拉伸)
- (UIImage *)imageByResizeToSize:(CGSize)size
{
return [self imageByResizeToSize:size scale:YES];
}

// 根据指定尺寸及是否按屏幕分辨比放大,调整图片尺寸(图片可能会被拉伸)
- (UIImage *)imageByResizeToSize:(CGSize)size scale:(BOOL)scale
{
if (size.width <= 0 || size.height <= 0) return nil;
CGFloat scaleFactor = scale ? self.scale : 1.0;
UIGraphicsBeginImageContextWithOptions(size, NO, scaleFactor);
[self drawInRect:CGRectMake(0, 0, size.width, size.height)];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

这样调整后的图片尺寸,若缩放比例不一致,会出现图片拉伸变形。如需等比缩放,我们可对方法再做一步处理,指定图片形变宽度,比例缩放图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 根据指定宽度,等比例调整图片尺寸
- (UIImage *)imageByResizeToWidth:(CGFloat)width
{
return [self imageByResizeToWidth:width scale:YES];
}

// 根据指定宽度及是否按屏幕分辨比放大,等比例调整图片尺寸
- (UIImage *)imageByResizeToWidth:(CGFloat)width scale:(BOOL)scale
{
if (self.size.width <= 0 || self.size.height <= 0) return nil;
CGFloat height = width * self.size.height / self.size.width;
return [self imageByResizeToSize:CGSizeMake(width, height) scale:scale];
}

之后,则是进行图片大小压缩处理。由于UIImageJPEGRepresentation()方法中的compressionQuality参数是0到1区间的浮动数值,若通过设定统一数值(如0.5)进行压缩,无法应对指定压缩大小的情况,因而我们一般会选用迭代逼近的办法,不断调整压缩质量,获取满足图片大小的图片数据。具体思路为,先判断压缩质量1.0的图片数据大小是否满足要求,否则根据递减系数循环压缩,满足条件或压缩至最低压缩质量则结束循环。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (NSData *)compressToJPEGFormatDataWithFactor:(CGFloat)factor maxFileSize:(u_int64_t)fileSize
{
if (!self) return nil;

NSData *tempImageData = UIImageJPEGRepresentation(self, 1.0);
if ([tempImageData length] <= fileSize) return tempImageData;

CGFloat compression = 1.0f;
CGFloat minCompression = 0.1f;
CGFloat compressionFactor = factor; // 0.1 0.01

while (compression > minCompression)
{
@autoreleasepool
{
compression -= compressionFactor;
tempImageData = UIImageJPEGRepresentation(self, compression);

if ([tempImageData length] <= fileSize) return tempImageData;
}
}

return nil;
}

以上迭代逼近取值,采用的是普通的迭代循环的办法。这里存在两个问题,首先是顺序循环下,查找效率较低。再而,需手动设置递减系数。同时,由于递减系数决定查找次数(0.1 -> 10, 0.01 -> 100),进而决定结果精度,高精度必然导致低效率。若要保持一定的结果精度同时提高查找效率,我们可以选择使用二分查找的办法,调整查找区间,减低查找次数,逼近最优结果。查找过程从compressionQuality的中间值(0.5)开始,如果中间值满足要求,结束查找;如果结果对应的compressionQuality值大于或者小于中间值,则在大于或小于中间值的新区间中查找,并且和开始一样从中间值开始比较。若区间下界大于或等于上界,结束查找。如下图所示:

具体实现如下:

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
// 限定压缩精度系数
NS_INLINE CGFloat clampCompressionFactor(CGFloat factor)
{
return factor <= 1e-10 ? 1e-10 : factor > 0.1 ? 0.1 : factor;
}

// 根据压缩精度系数([1e-10, 0.1])及文件大小,获取压缩后的JPEG格式的图片数据
- (NSData *)compressToJPEGFormatDataWithFactor:(CGFloat)factor maxFileSize:(u_int64_t)fileSize
{
if (!self) return nil;

NSData *tempImageData = UIImageJPEGRepresentation(self, 1.0);
if ([tempImageData length] <= fileSize) return tempImageData;

NSData *targetImageData = nil;
CGFloat compressionFactor = clampCompressionFactor(factor);
CGFloat minFactor = 0;
CGFloat maxFactor = 1.0;
CGFloat midFactor = 0;

while (fabs(maxFactor-minFactor) > compressionFactor)
{
@autoreleasepool
{
midFactor = minFactor + (maxFactor - minFactor)/2;
tempImageData = UIImageJPEGRepresentation(self, midFactor);

if ([tempImageData length] > fileSize)
{
maxFactor = midFactor;
}
else
{
minFactor = midFactor;
targetImageData = tempImageData;
}
}
}

return targetImageData;
}

这里,由于compressionQuality为浮点值,设置查找出口时,若简单判断查找区间下界和上界差值(maxFactor-minFactor > 0),下界恒小于上界,将陷入死循环。因而,我们可以选择设定查找区间下界和上界的差值精度(maxFactor-minFactor > 0.01)以避免这种情况。同时,通过内联函数clampCompressionFactor()限定精度范围([1e-10, 0.1])。之后,则是对方法接口进一步处理,方便外部调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 指定图片宽度及图片文件大小,压缩图片,图片将等比例缩放
- (NSData *)resetImageDataWithImageWidth:(CGFloat)width maxFileSize:(uint64_t)maxFileSize
{
// Image Size
UIImage *newImage = [self imageByResizeToWidth:width];

// File Size
return [newImage compressToJPEGFormatDataWithFactor:1e-10 maxFileSize:maxFileSize];
}

// 指定图片尺寸及图片文件大小,压缩图片(图片可能会被拉伸)
- (NSData *)resetImageDataWithImageSize:(CGSize)size maxFileSize:(uint64_t)maxFileSize
{
// Image Size
UIImage *newImage = [self imageByResizeToSize:size];

// File Size
return [newImage compressToJPEGFormatDataWithFactor:1e-10 maxFileSize:maxFileSize];
}

以上方法中,我们设置图片压缩精度系数为1e-10,若对查找结果要求不高,可按实际需求进行调整。最后,在需要的地方调用即可:

1
2
3
UIImage *image = [UIImage imageNamed:@"test_1.jpg"];

NSData *data = [image resetImageDataWithImageWidth:640 maxFileSize:100000];

示例代码:https://github.com/ColinHwang/Demo-of-Tutorial-in-Blog/tree/master/iOS-CompressImage

参考资料:

[1]SuperDanny.iOS图片压缩上传[EB/OL].http://superdanny.link/2016/01/28/iOS-Upload-Image ,2016-1-28.
[2]GeekDmm.(3)iOS程序猿算法学习——二分查找「Binary Search」[EB/OL].http://www.jianshu.com/p/30906f7014ec ,2016-2-27.
[3]JasonDing.【leetcode边做边学】二分查找应用[EB/OL].http://www.jianshu.com/p/ff2c4ab66f98 ,2014-9-18.
[4]Apple Inc.UIImage Reference Class[EB/OL].https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIImage_Class/index.html ,2016-7-30.