Android PNG图片像素检测及剪裁优化


PNG图片是当前移动终端最主流的图片格式之一,由于android中大部分图片颜色数比较小而且尺寸也不大,所以在各类app软件中PNG图片几乎是首选的图片格式。在手Q中PNG图片大概有四五千张。如此众多的PNG图片是android安装包资源以及内存占用的大头消费者。大家都知道,在android中,有一种特殊的PNG图片形式 .9.png,俗称点九图,也叫九宫图。在android平台下使用点九PNG技术,可以将图片横向和纵向同时进行拉伸,以实现在多分辨率下的完美显示效果。但是对这种点九图,是否还存在优化的空间呢?是否可以进一步剪裁呢?因为本身就可以拉伸扩展,我们的图片是否可以缩小到极致,另外对颜色区域比较规范的PNG图片是否也可以转换为点九图?一方面可以减少安装包的大小,同时在绘制展示图片的时候,也可以节省系统内存。基于这种思路,本文尝试使用一种工具,来实现png图片的优化处理。

要进行剪裁处理,首先我们需要了解下PNG图片的文件格式。

 

 

PNG格式分类

PNG格式主要有8位、24位、32位三种形式,其中8位PNG支持两种不同的透明形式(索引透明和alpha透明,索引透明仅支持全透明,alpha透明支持半透明),24位PNG不支持透明,32位PNG在24位基础上增加了8位透明通道,因此可展现256级透明程度。

在android中,我们经常用到的是png8和png32两种格式。

索引透明的png8只有256色,通过一个值表明某个像素点是否透明。

Alpha透明的png8也有256色,但它可以指定某个像素点的透明度,支持半透明。但这种alpha信息在PS中读取不了。

PNG8(Alpha)只有256色,它通过8位索引值在调色板中索引一个颜色表示,因此在颜色数方面比PNG32少很多,所以占用的空间会比PNG32小很多,而对于大多数的图标、按钮和背景等常使用PNG格式的图片来说,256色基本都可以胜任,因此在我们制作图片资源的时候,如果图片质量可以接受尽量使用PNG8格式图片。一些有损压缩工具大多也是将小于256色的PNG32图片压缩为PNG8(alpha透明)。

PNG文件解析

PNG文件主要由一个文件标示或文件头域和至少3个数据块组成。其中文件头固定为以下格式用来识别PNG:89 50 4E 47 0D 0A 1A 0A(十六进制)

第一个字节0x89超出了ASCII字符表示的范围,可以避免某些软件将PNG文件当做文本文件来处理。4E 47 0D三个字节是“PNG”三个字母对应的ASCII码,后面的四个字节表示了不同格式的换行符等固定的标识。

除了特定的文件头外,后续是由一个个按照顺序排列的PNG的数据块(Chunk)。

PNG定义了两种类型的数据块:一种是PNG文件必须包含、读写软件也都必须要支持的关键数据块(critical chunk),关键数据块定义了4个标准数据块:IHDR, PLTE, IDAT, IEND;另一种叫做辅助数据块(ancillary chunks),PNG允许软件忽略它不认识的附加块。这种基于数据块的设计,允许PNG格式在扩展时仍能保持与旧版本兼容。

PNG中数据块的类别如下表,其中关键数据块使用橙色背景区分:

PNG文件格式中的数据块

数据块符号

数据块名称

多数据块

可选否

位置限制

IHDR

文件头数据块

第一块

cHRM

基色和白色点数据块

在PLTE和IDAT之前

gAMA

图像Y的数据块

在PLTE和IDAT之前

sBIT

样本有效位数据块

在PLTE和IDAT之前

PLTE

调色板数据块

在IDAT之前

bKGD

背景颜色数据块

在PLTE之后IDAT之前

hIST

图像直方图数据块

在PLTE之后IDAT之前

tRNS

图像透明数据块

在PLTE之后IDAT之前

oFFs

(专用公共数据块)

在IDAT之前

pHYs

物理像素尺寸数据块

在IDAT之前

SCAL

(专用公共数据块)

在IDAT之前

IDAT

图像数据块

与其他IDAT连续

tIME

图像最后修改时间数据块

无限制

tEXt

文本信息数据块

无限制

zTXt

压缩文本数据块

无限制

dRAx

(专用公共数据块)

无限制

gIFg

(专用公共数据块)

无限制

gIFt

(专用公共数据块)

无限制

gIFx

(专用公共数据块)

无限制

IEND

图像结束数据块

最后一个数据块

         

 

针对于每个数据块,又主要由4部分组成:

单个数据块结构

名称

字节数

说明

长度(Length)

4字节

制定数据块中数据域的长度,不超过(231-1)字节

数据块类型码(Chunk Type Code)

4字节

数据块类型码有ASCII字母(A-Z和a-z)组成,即上表的数据块符号

数据块数据(Chunk Data)

可变长度

存储按照Chunk Type Code 指定的数据

循环冗余检测(CRC)

4字节

存储用来检测是否有错误的循环冗余

     

CRC(cyclic redundancy check)域中的值是对Chunk Type Code域和Chunk Data域中的数据进行计算得到的,具体算法可以参照ISO相关文档。

由上面的表我们发现很多数据块其实是我们不需要的,所以针对PNG图片的很多压缩工具,无损压缩主要是处理这些非必须的数据块来减少PNG的体积。下面我们了解下PNG的关键数据块:

IHDR(文件头数据块)

该数据块是第一个数据块,而且必须是第一个,并且仅能有一个,它包含了PNG图片的主要信息,主要包括了图片的宽度和高度(4bytes表示),以及图片的位深度,颜色类型,压缩以及扫描方法等(1byte),该数据块固定为13bytes长度,因此我们可以通过00 00 00 0D 49 48 44 52来定位该数据块(前四位表示13个字节长度,后四位表示“IHDR”的ASCII码)。

如上图所示,图片宽度为 00 00 00 36即54像素,图像高度为:00 00 00 20 即32像素。08表示色深为8,28=256色位真彩色,06代表带alpha通道 8位alpha,即图片格式为PNG32,压缩类型0000表示使用Deflate压缩编码压缩图像数据;接着的00表示为将来使用更好的压缩方法预留;然后是表示非隔行扫描00后面的59 ca 8b 5b为循环冗余检测。

PLTE(调色板数据块)50 4C 54 45

调色板数据块包含有与索引彩色图像相关的彩色变换数据,它仅与索引彩色图像有关(PNG8),该数据块定义了图像的调色板信息,可以包含1-256个调色板信息,每个调色板信息包含RGB颜色值。因此调色板长度必须是3的倍数,否则就是非法的调色板。比如,我们有一个png8格式的图片,在图像数据块中,颜色值都是一个个的索引值,当解析和渲染图片时,我们可以通过这个索引值到PLTE调色板数据块中查找到相应的RGB颜色值。对应关系如下图所示:

对于索引图像,必须要有调色板信息,调色板的颜色索引从0开始编号,颜色数不能超过色深中规定的颜色数(如色深为4,调色板颜色数不能查过24=16),否则导致图片不合法。

如上图所示,表明PLTE的长度为15, 50 4C 54 45 为PLTE标示位,后面就是索引数据。一直到 00 00 00,后面的08 88 39 af为循环冗余检测。

真彩色图像和带α通道数据的真彩色图像也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。

在这里顺便介绍下tRNS块,这个块可有可无,主要是看是否使用了透明色,该区块可以理解为透明度的索引板,循环长度为调色盘的颜色数,相当于调色盘颜色表的一个对应表,标识该颜色是否透明,0xFF不透明,0x00透明。

IDAT(图像数据块)

图像数据块IDAT(Image Data Chunk):它存储了实际的数据,在PNG文件数据中可包含多个连续顺序的图像数据块。

如上图所示: 表明该图片数据长度为130,其中49 44 41 54 为IDAT数据块的标示。后面跟着的就是IDAT存放着的图像真正的数据信息,因此,如果能够了解IDAT的结构,就可以很方便的生产PNG图像。IDAT 区块是经过压缩的,所以数据不可读。

IEND(图像结束数据块)

图像结束数据块(Image Trailer Chunk):用来标识PNG文件或者数据流的结束,并且必须要放在文件的尾部。

通常情况下,文件的结尾12个字符:00 00 00 00 49 45 4E 44 AE 42 60 82

由数据块结构定义可知:IEND数据块长度是0(00 00 00 00),数据标识IEND(49 45 4E 44),CRC码(AE 42 60 82)。

PNG图片剪裁优化思路

其实点九图也是PNG格式图片,它是PNG图片的子集,和PNG图片相比,它只是在四周各增加了一行像素点,增加的像素点只有黑色和透明两种,黑色像素点标明拉伸区域:最上面的一行像素点通过黑色区域标示水平可拉伸区域,左边的一列像素点表明垂直拉伸区域,两者重合部分可以全拉伸。下边和右边的像素点标示改图片用于资源时,其上的内容的填充区域。

因此,既然存在拉伸区域,拉伸前像素点完全相同的若干行拉伸后的效果和单一行拉伸后的效果是一样的,因此就可以裁剪为一行,这样标注起这一行可以拉伸,就可以将我们裁剪掉的像素点通过拉伸的方式填充进来,这也是我们裁剪点9图图片的主要思路。

同样,对于PNG图片,我们可以通过相同的思路,逐行扫描像素点完全相同的行和列,然后只保留一行(列),然后,对PNG图片四周各添加一行像素值,对保留的行和列通过描点标示可以拉伸,填充区域由于之前是PNG,所以我们设置为全部行和列都可以填充,最后修改图片的后缀为.9.png,这样就可以将PNG图片转换为点9图。

Png剪裁实现

在处理PNG图片时,我们需要逐行扫描出该图片中哪些行像素点是相同的,秉承不重复造轮子的原则,我们选用了一个第三方的库。针对png处理的库有很多,这里我们选择了完全使用JAVA实现的pngj库来实现PNG图片的编解码。该库只是个很小的处理单元,不涉及到相关的压缩处理,当然也没有库依赖。因为功能单一,所以处理速度也比较理想。最主要的是它能够实现逐行读取图片的原生数据信息。所以可以允许我们针对每个像素点做处理工作。下面是该库的一些介绍:

 Very efficient in memory usage and speed.

 Pure Java (requires 5 or greater).

 Small and self contained. No dependencies on third party libraries,

 Allows to read/write progressively, row by row. This means that you can process huge images without needing to load them fully in memory.

 Reads and writes all PNG color models (true color, gray, palette, all bit depths: 1-2-4-8-16, with-without alpha). Interlaced PNG is supported (though not welcomed) for reading.

 Full support for metadata ("chunks") handling.

 The format of the pixel data (read and write) is extensible (since 2.0) and very efficient (no double copies).

 Supports (since 2.0) asyncronous reading and low level tweaking and extension in the reader.

 Note that this is a relatively low-level library. It does not provide any high-level image processing (eg, resizing, colour conversions), nor tries to abstract the concrete image format (as BufferedImage does, for example). In particular, the default format of the scanlines (as wrapped inImageLineInt or ImageLineByte) is not abstract, the meaning of the values depends on the image color model and bitdepth.

实现png剪裁,首先,我们需要设定一个扫描目录,和一个输出目录,扫描目录可以选择android工程的drawable文件目录。

1,首先需要先扫描待裁剪的png图片。看下是否存在剪裁空间。优化比例是否值得我们做剪裁操作:先初始化PngReader:

PngReader pngr = null;

try{

pngr = new PngReader(file);

}catch(PngjInputException e){

System.err.println(filename + " read error");

return null;

}

pngReader会解析图片文件的相关结构,获取到图片的格式,channel,位深度,alpha,宽高等图片基本信息存储到ImgInfo中。获取这些数据可以计算出原始图片面积,原始图片的基本信息等属性。通过函数

line = (ImageLineInt) pngr.readRow()).getScanline()

逐行扫描图片的每一行数据,然后将数据存储到一个int[]数组中:

int[] lineI = Arrays.copyOf(line, pngr.imgInfo.cols * channels);

这个lineI数组,存储着每个像素点的数据,如果为png8,那么每一个int保存一个像素点数据,如果为png32,每四个int存储一个像素点,分别代表RGBA数据。这样可以再接着扫描第二行..第n行,获取所有像素数据。因此现在的问题就变成了从一个类二维数组中查询出所有连续相同的列和连续相同的行的问题,这是个纯数学的算法问题,这里不再详述。需要注意的是:因为.9.png四个边各有一个像素点的扩展区域标示, 表示可拉伸区域和可填充区域。所以在扫描时比对像素点是否相同需要去除这个区域,每行比较时去除第一和最后一个像素点,扫描开始于第二行,结束于n-1行。

通过一轮扫描,我们可以获取到图片可剪裁的优化区域:连续相同的列可以合并成一列,连续相同的行可以合并成一行。然后针对合并后保留的那一行(列),标示为可拉伸的区域。因为原始的.9.png也存在拉伸区域,我们这次扫描出的拉伸区域要和原来.9.png的图片拉伸区域进行一次对比,如果两者没有重合·,就忽略处理(防止出现多条间断的拉伸区间)。如果重合就使用两者并集。

2,扫描完数据后,为了便于观察结果,会将每一张图片的可优化区域通过一个excel表的形式进行输出,包含:图片原始大小,图片可裁剪行列的起止位置。图片可优化面积,可裁剪的比例等信息。

3,裁剪:需要将连续相同的行(列)去除,只保留一行(列)。去除之后,还需要标示拉伸区域,如果为png还需要转换为.9.png。

针对边缘描边:可拉伸区域为黑色。不可拉伸区域为透明。但是针对png8格式的png图片,图片中只保存了各个像素点的索引值,而不是具体的RGB数值。所以针对这种格式的png图片我们没法确认黑色值和透明值具体的索引是多少,所以这种格式的图片只是提供裁剪优化指引,而没有进行裁剪处理。

针对.9.png。我们在第一轮扫描第一行的时候,可以利用这个时机记录下两个值:第一行第一个像素点肯定是透明的,后续我们填充不可拉伸区域就用这个值。第一行里和第一个像素点不同的像素点肯定是记录了黑色的RGBA或者索引值,我们可以根据这个值来标示我们剪裁后图像的可拉伸区域。针对png32的png图片,透明值为:255.255.255.0,黑色为:0.0.0.255.

4,输出新的图片

确定了剪裁区域,我们可以把我们新图的一些属性信息通过新建一个ImageInfo存储下来。然后新建pngWriter:

PngWriter pngw = new PngWriter(outFile, newInfo, true);

//如果是索引图片,需要拷贝全部的Chunk信息,主要是需要包含索引板

if (info.channels == 1 && info.indexed) { 

isPalettedImage = true;

pngw.copyChunksFrom(pngr.getChunksList(), ChunkCopyBehaviour.COPY_ALL);

} else {

isPalettedImage = false;

pngw.copyChunksFrom(pngr.getChunksList(), ChunkCopyBehaviour.COPY_ALL_SAFE);

}

 

读取原始图片每一行的数据

ImageLineInt line= (ImageLineInt)pngr.readRow();

int[] oldline = line.getScanline();

按前面的规则修改:(边界的话,重新确立可拉伸区域,内容,删除重复的列所对应的像素点,只保留一列)

ImageLineInt lineInt = new ImageLineInt(newInfo, firstPixs);

pngw.writeRow(lineInt);

这样完成图片所有数据的更改,生成新图,输出到目录中。针对png32格式的png图片,我们需要新增四个边界处的像素点,并修改为.9.png格式为图片文件。

目前该小工具已经接入手Q中的自动化工具,可以针对手Q中的图片资源进行扫描,发现可以优化的图片时,会自动提单给相应的开发。由于这种方式的修改图片粒度比较低,所以,在应用到手Q中时,我们针对处理进行了一次过滤:设定扫描图片面积阈值为50*50像素点,单行可优化像素点最低为50,优化比例为大于45%。剔除比较小的优化点。

如下图展示了输出的可裁剪图片的汇总表格:

针对单张图片优化前后的对比如下:

优化前:优化后:

 

该png剪裁工具可以高效率实现扫描png图片是否存在剪裁压缩空间的目的,并且可以根据扫描出的优化比例自动修正图片,输出更节省空间和内存的新图片。并支持转换png图片为.9.png的功能。但同时也存在一定的不足:比如一些特殊的png图片在特定环境下不需要剪裁; 剪裁的选择区域有时候不够智能, png图片同时存在行和列的优化区域存才可转换成点9; png8格式的png图片,使用索引值表征像素点数据所以只能给出裁剪建议,而没有生成裁剪后的新图等问题。

转:http://mp.weixin.qq.com/s?__biz=MzA4MDc5OTg5MA==&mid=401181863&idx=2&sn=e3562efa052c9622d5140d52c4fc25d5&scene=23&srcid=1217QFsx12bDGwGN6dj6kZw2#rd

posted @ 2015-12-17 18:14  nick_leeli  阅读(3178)  评论(0编辑  收藏  举报