浅谈移动端图片压缩

一、前言

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

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

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

二、一些基础知识

所谓大小

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

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

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

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

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

现实与里世界

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

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

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

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

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

三、压缩方法

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

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

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

二分法

objectivec 复制代码
+ (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张图片后对微信算法的总结与归纳 发表了一个第三方库

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

java 复制代码
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;
  }
}

源码优化

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

java 复制代码
else {
    // return longSide / 1280 == 0 ? 1 : longSide / 1280;
    // 代码之前已经判断了 longSide < 1664 不会再出现 < 1280 的情况了
    return longSide / 1280;
}
java 复制代码
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压缩算法

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

csharp 复制代码
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

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui