一、前言
图片基本上是目前移动端无法逃避的内容 最基础的用户头像、相册、评论都有涉及
涉及到了前端与后端的交互 那么一定会考虑到图片压缩
本篇文章就是作者近期涉及到图片压缩所踩到的一些坑
二、一些基础知识
所谓大小
图片分为两个大小 分辨率大小 与 文件大小
通常文件大小与分辨率大小是有正相关关系的
分辨率高 * 分辨率宽 = 像素点数量
像素点的格式化方式、文件格式、压缩率 等等影响到 文件大小
而图片压缩的最终目的 就是 获取更小的图片文件大小
现实与里世界
通常我们看到的某张图片是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; 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; }
|
1 2 3 4 5 6
| else { 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