专栏名称: 移动开发前线
专注于分享移动开发前沿和一线技术。
目录
相关文章推荐
51好读  ›  专栏  ›  移动开发前线

再谈图片压缩

移动开发前线  · 公众号  · 前端  · 2017-05-02 19:17

正文

版权声明

作者:郑晓勇

原文:http://zhengxiaoyong.me/2017/04/23/也谈图片压缩/

本文为作者投稿,未经允许禁止转载。


简介


随着目前设备像素的不断提高,基本随便一张照片就是M级别的大小,对于如此大的图片,不管是在内存空间、带宽资源和服务器数据空间上都是非常耗费的,特别是在移动端,由图片引起的OOM和图片上传质量过大等问题我想大家都遇到过,所以对于图片内存占用上和物理空间占用上进行压缩很有必要。

在Android上,我们使用到的图片格式无非这五种:PNG、JPEG、Webp、SVG、GIF。其中GIF的位深为8位,所以文件通常比较小而且支持alpha通道以及动画,Webp在等质量的大小上和等大小的清晰度上都占极大优势,而SVG矢量图是由xml文件进行描述的,可以适配于任何分辨率的设备而保证图像不失真,Google的官方视频中也提到可用这两种格式进行某些场景下替换PNG或JPEG图像,这不但能节约带宽资源还能提高图片加载速度。

所以图片压缩主要是对PNG和JPEG这两种格式,关于图片的压缩,有无损压缩和有损压缩两种方式,这两种压缩方式区别如下:

  • 无损压缩 :通过对冗余数据的存储方式进行优化,该方式不会丢失文件内容,压缩率受冗余度的影响,所以压缩率较低

  • 有损压缩 :通过丢失不会对文件造成太大影响的数据来达到压缩效果,所以压缩率较高

其中PNG是无损压缩格式图片,JPEG是有损压缩格式图片,所以对应的也有各自的压缩算法。

在Android系统中,png的压缩是使用libpng进行压缩,场景有两个:编译阶段以及api层调用方式进行压缩。

其中在编译阶段通过aapt打包工具会对drawable目录下png图片进行压缩,压缩率大约在40%以下,如果我们对编译后的apk进行解压,可以发现解压后drawable目录下的png图片比原先的变小了。但是,也有例外, NinePatch(.9) 图片经过编译反而变大,因为对于 .9 图片在编译过程中aapt会对它额外进行处理,使得 .9 图片会增加2~3个不同类型的 Chunk 块( :api层调用方式进行压缩不会对 .9 进行额外处理)。

jpeg的压缩是用libjpeg(7.0后有变化,后面另外说)进行压缩,场景只有在api层进行调用方式进行压缩。下面将主要围绕图片的压缩原理、压缩策略以及在Android上的运用进行讲解。


色彩空间


在此之前,我们先了解一下ColorSpace(色彩空间)。通常来说,我们看一张彩色图片,会有几千甚至上万种颜色构成,色彩空间就是用来表示图片所构成的色彩范围。对于图片最常用的是使用三原色组成的RGB色彩空间,它最大可表示2^24(16777216)种颜色。

其它的色彩空间还有YUV、CMYK、YCCK等,其中YUV在视频的开发方面会经常涉及到,它主要用于表示彩色视频中彩色图像的颜色空间,因为它节约带宽,每个像素位深最大不超过12位,最小为6位,在此不过多描述,了解即可。

上面说到RGB色彩空间,它根据每个分量的所占位数不同又可以分为这两种:RGB_565、RGB_888,其中带alpha通道的有这两种:ARGB_4444、ARGB_8888,区别如下:

  • RGB_565 :每个像素占两个字节,R分量占5位,G分量占6位,B分量占5位,最多能表示2^16(65536)中颜色

  • RGB_888 :每个像素占三个字节,R、G、B分量各占8位,最多能表示2^24(16777216)中颜色

  • ARGB_4444 :每个像素占两个字节,A、R、G、B分量各占4位,最多能表示2^12(4096)中颜色,成像效果比较差,所以Google给了它一个Deprecated,并且v4.4+后如果使用了它会自动转成用ARGB_8888

  • ARGB_8888 :每个像素占四个字节,A、R、G、B分量各占8位,最多能表示2^24(16777216)中颜色,其中前面8位alpha(0~255)通道表示每个像素点的透明度


图片存储形式


图片的存储形式,主要以以下三种形式存在:File、Stream、Bitmap。其中在Android上File主要有PNG、JPEG、XML(VectorDrawable)、Webp和GIF这五种类型格式进行存储,下面分别对这三种存储形式以及压缩方式进行分析。


PNG


PNG是一种无损压缩的图像存储格式,正由于它使用的是无损压缩算法进行压缩,所以相同像素宽高的图像保存为PNG在文件大小上比JPEG往往要大的多,一般是JPEG大小的几倍左右。由于无损压缩不会丢失图像数据,并且支持alpha通道而且完整的保存了图像数据且无锯齿,所以一般应用在PS素材或图标上,这就为什么不管Android和iOS图标都是使用的png格式。

PNG图像根据每个像素位数的不同,可分为三种格式:PNG8、PNG24、PNG32。PNG8只支持256色,有索引色透明和Alpha透明两种方式,索引色透明只能简单的指明一个像素点为透明还是不透明,Alpha透明则支持像素点的透明度,PNG24支持全色1670万色,只支持不透明,PNG32支持全色1670万色,在PNG24基础上增加了8位的alpha分量,支持Alpha透明,目前大部分PNG图片使用的格式大都为PNG32。


PNG数据结构


一个标准PNG图像文件数据结构如下:

其中一个最简单的PNG图像应该至少包含PNG文件的签名 Signature 、文件头数据块 IHDR 、图像内容数据块 IDAT 以及图像结束数据块 IEND 。这三个的数据块叫做关键数据块,是每一个PNG文件必须包含的,否则PNG文件将无法正常显示,另外还有 辅助数据块 ,如: PLTE (调色板数据块,仅用于索引PNG)、 tEXt (文本信息数据块),这些辅助数据块是可选的,用于额外表示一个PNG文件的内容。

这些Chunk都由四部分内容组成:

含义如下:

  • Length :占4个字节,表示该Chunk中Data域的长度

  • Type :占4个字节,表示该Chunk的类型,如:IHDR、IDAT等

  • Data :占n个字节,存储着该Chunk的数据

  • CRC :占4个字节,循环冗余校验码

下面主要说下这三个关键数据块,它们表示的含义如下:

  • Signature :占8个字节,用于表示该文件是一个PNG文件,内容固定

  • IHDR :占25字节,其中Data域占13个字节,用于表示图像的基本信息,如图像的宽高与位深等,并且它永远都是第一个数据块

  • IDAT :占n个字节,用于表示图像的数据信息,它存储真实的图像数据,在一个PNG文件中,该数据块出现的数量为>=1

  • IEND :占12个字节,用于表示数据块内容已结束,永远都是最后一个数据块,内容固定

如下,我们查看一张最简单的PNG文件结构:

可以看到这张PNG图像只包含了 Signature IHDR 、一个 IDAT 以及结束数据块 IEND ,可以说是最简单的PNG图像。

前8个字节:

89 50 4E 47 0D 0A 1A 0A

描述的 Signature 为ASCII字符 .PNG 表示该文件为PNG文件。

后续的25个字节:

00 00 00 0D 49 48 44 52 00 00 00 30 00 00 00 30 08 06 00 00 00 57 02 F9 87

描述的是 IHDR 头数据块,其中前4个字节 00 00 00 0D 表示该数据块Data域的长度为13字节,然后是4个字节 49 48 44 52 描述的该数据块类型,对应的ASCII字符为 IHDR ,接下来是数据块真正存储的数据Data域,最后是4个字节的CRC校验码。关于 IHDR 数据块的Data,主要有四个我们比较关心的数据:图像宽高、色深以及颜色类型。其中宽和高各占4个字节,位深和颜色类型占1个字节,对应字节为:

00 00 00 30 00 00 00 30 08 06

从中可以得知该PNG图像宽和高都为 00 00 00 30 (48px),色深为8bit,颜色类型为6(6代表带alpha通道的彩色图像)。

更多细节可以参考W3C对于PNG的标准定义:

https://www.w3.org/TR/PNG/

NinePatch(.9)

NinePatch(.9)图片是Android上一个可动态伸缩的PNG图片,原理是在PNG图像基础上添加额外的1像素的边框来描述动态伸缩与内容填充的区域,然后在编译打包时通过aapt工具对.9图进行额外处理。

具体是提取所添加的1像素边框的信息,这些信息会通过额外类型 npTc npOl Chunk 数据块保存在PNG文件中,当在图片加载时,会在判断该图片是否为.9图来选择性的构造一个 NinePatchDrawable 还是 BitmapDrawable 对象, NinePatchDrawable 即是一个可对内容区域进行动态伸缩的Drawable,判断是否为.9图以及构造一个 NinePatchDrawable 代码为:

        byte[] npChunk = bitmap.getNinePatchChunk();
        if (npChunk != null && NinePatch.isNinePatchChunk(npChunk)) {
            NinePatchDrawable npDrawable = new NinePatchDrawable(getResources(), bitmap, npChunk, new Rect(), null);
            //...
        }

一个.9图片可以由自带的draw9patch工具进行制作,制作后文件的后缀为 .9.png

PNG压缩

关于对PNG图片的压缩,Android默认使用的是libpng库进行PNG图片的压缩,场景有两个地方:aapt打包时和bitmap.compress()时。

所以对于Android中PNG的压缩或想获取更好的压缩率,我们有两种做法:

  1. 屏蔽在aapt打包时默认的libpng的压缩,我们自己使用第三方压缩工具进行png图像的压缩

  2. 对于api层面,使用自己编译的lib库替换系统的api进行png的压缩

对于一些第三方png压缩工具,有:

通常来说如果我们不满足于在aapt打包时进行的png图片压缩,我们可以通过上面的工具进行png的压缩,此时,必须屏蔽aapt打包时的压缩。

我们可以通过gradle的 aaptOptions 配置来屏蔽aapt打包时对png进行压缩,进而使用我们自己压缩的png图片,通过以下配置:

android {
    aaptOptions {
        cruncherEnabled false
    }
}

其中这不会屏蔽对.9图片的处理,所以不影响.9图的使用。


JPEG


JPEG是一种有损压缩的图像存储格式,不支持alpha通道,由于它具有高压缩比,在压缩过程中把重复的数据和无关紧要的数据会选择性的丢失,所以如果不需要用到alpha通道,那么大都图片格式都用该格式。

JPEG数据结构

一张JPEG图片的数据结构大致如下:

JPEG文件主要是由多个 segment 段组成,每个 segment 又由 标识码 压缩数据 组成,标识码由两个字节组成,第一个为固定值 0xFF ,而区分每个标识码的类型主要由第二个进行区分,下面介绍一下常用的标识码:

  • FFD8 :表示图像的开始,段名为 SOI

  • FFE0 :表示JFIF数据块,段名为 APP0

  • FFC0 :表示图像帧开始,段名为 SOFO

  • FFC4 :表示Huffman表,段名为 DHT

  • FFDA :表示从上往下开始扫描图像,段名为 SOS

  • FFD9 :表示图像结束,段名为 EOI

更多JPEG格式细节可以看JPEG Wiki:

https://en.wikipedia.org/wiki/JPEG







请到「今天看啥」查看全文


推荐文章
21世纪经济报道  ·  推动县域经济发展,华夏幸福实践探索获认可
8 年前
张德芬空间  ·  ​一个处处防备的人,无法靠近别人
8 年前
三体迷  ·  土星环中神奇的螺旋桨
7 年前