金沙棋牌官方平台

当前位置:金沙棋牌 > 金沙棋牌官方平台 > 图片压缩算法,获取图片信息和像素内容

图片压缩算法,获取图片信息和像素内容

来源:http://www.logblo.com 作者:金沙棋牌 时间:2019-11-09 19:47

png的故事:获取图片信息和像素内容

2017/03/25 · JavaScript · 1 评论 · PNG

原文出处: AlloyTeam   

对于一个PNG文件来说,其文件头总是由位固定的字节来描述的,HEX: 89 50 4E 47 0D 0A 1A 0A

png的故事:隔行扫描算法

2017/06/21 · 基础技术 · PNG

原文出处: AlloyTeam/june01   

import java.io.DataInputStream;
import javax.microedition.lcdui.Image;
public class Tools {
private static final int FLAG_16BIT_4_LEN = 0;
private static final int FLAG_REBUILD_SIZE = 0;
private static final int FLAG_REBUILD_MODULE = 0;

前言

现在时富媒体时代,图片的重要性对于数十亿互联网用户来说不言而喻,图片本身就是像素点阵的合集,但是为了如何更快更好的存储图片而诞生了各种各样的图片格式:jpeg、png、gif、webp等,而这次我们要拿来开刀的,就是png。

使用ultra打开一个png图片,结果如下:

前言

前文已经讲解过如何解析一张png图片,然而对于扫描算法里只是说明了逐行扫描的方式。其实png还支持一种隔行扫描技术,即Adam7隔行扫描算法。

* 1 压缩原理 要清楚 USI 的压缩原理,首先需要对图像的存储方式有一个基本的了解。USI 压缩是建立在索引色的基础上进行的。
*
* 1.1 索引图与RGB图
* 对于PNG图像,可以分为索引(Index)图和RGB图两种,索引图只包含固定数量的颜色,而RGB图的颜色数量是不受限制的。
* RGB图的每一个象素都保存一个RGB值,代表这个象素的颜色,因此,一张RGB图有多少个象素,文件中就保存多少个RGB值。
* 而索引图会将其固定数量的颜色,按照顺序排列起来,作为颜色的索引保存在文件头中,被称为调色板(palette)。每一个
* 象素只保存其颜色在调色板中的索引。如一个32色的索引图,在文件头中保存了32个颜色,索引值从0到31。图中每一个象
* 素只记录其颜色的索引。因此,对于一般的PNG图,索引图文件的大小总是小于RGB图的。
*
* 1.2 行程压缩原理
* 当我们把一张索引图的所有象素(N个),按照从上到下,从左至右,即按行扫描的顺序排列起来的时候,我们得到一个队列。
* 如果我们用1个字节来存储一个象素的索引值(调色板颜色不超过256),那么数据的大小为N字节。这段数据的格式我们表示为
* [I1][I2]…[In] 共 N 个。在上面的队列中,可能会出现很多连续相同的索引值,最多的就是透明色。如果我们在每个索引值
* 前用1个字节保存这个值连续出现的数量(最多可以表示256个),那数据的格式变为[C1][I1][C2][I2]…[Cm][Im] 共 M个。
* 那么一张256个象素的单色图的所有数据,只需要2个字节来保存。通常,我们所需的图中总是有大片连续的颜色,包括透明色,
* 因此按照这个格式保存的图像,其文件大小可以大大降低,这就是行程的压缩原理。
*
* 1.3 USI压缩原理 如果一张索引图的颜色数为32,那么在[C1][I1][C2][I2]…[Cm][Im]
* 格式中,I的数值都小于32,那么每个字节前3 bits 始终为0。为了充分利用这 3bits,我们可以将 C 的值保存在这 3bits中,
* 这样我们的格式变为 [G1][G2]….[Gk] 共 K个(G的高位为数量,低位为颜色索引)。这样,对于32色的图,
* 每个字节最多可以保存8个象素的信息,对于64色的图,每个字节最多可以保存4个象素的信息,对于16色的图,每个字节最多
* 可以保存16个象素的信息。 在[G1][G2]….[Gk] 这K个字节前,再加上调色板数据和其它本图的必要信息,就得到了USI格式的文件。
*****************************************************************************************************************/
int m_flags ,m_count ,m_mask ,m_modelCount ,m_dataSize ;
int m_rebuildWidth,m_rebuildHeight;
int[][] m_pal ;
int []m_dataOffset;
byte[] m_models ,m_data ;
private void load(String file) {
try {
DataInputStream din = new DataInputStream(getClass()
.getResourceAsStream(file));
m_flags = din.readInt(); // 格式标志
 读取调色板信息 */
m_count = din.readByte() & 0xff; // 调色板位数
m_mask = 0xff >> (8 - m_count); // 计算 取色板索引的掩码
int pal_count = din.readByte() & 0xff; // 调色板数量
int pal_len = din.readByte() & 0xff; // 调色板长度 即颜色数
m_pal = new int[pal_count][pal_len]; // 初始化调色板容器
int pal;
// 读取调色板信息
for (int i = 0; i < pal_count; i++) {
for (int j = 0; j < pal_len; j++) {
pal = din.readShort() & 0xffff;
m_pal[i][j] = (((((pal & 0xF000) >>> 12) * (17 << 24)) & 0xFF000000)
| ((((pal & 0x0F00) >>> 8) * (17 << 16)) & 0x00FF0000)
| ((((pal & 0x00F0) >>> 4) * (17 << 8)) & 0x0000FF00) | ((((pal & 0x000F) * 17))));
}
}
读取图块信息 */
m_modelCount = din.readShort() & 0xffff; // 图块数量
// 读取图块尺寸
if ((m_flags & FLAG_REBUILD_SIZE) != 0) {
// 基于尺寸的转换方式
m_rebuildWidth = din.readByte() & 0xff;
m_金沙棋牌官方平台,rebuildHeight = din.readByte() & 0xff;
} else if ((m_flags & FLAG_REBUILD_MODULE) != 0) {
// 基于动画model的转换方式
m_models = new byte[m_modelCount * 2];
din.read(m_models);
}

简介

首先,png是什么鬼?我们来看看wiki上的一句话简介:

Portable Network Graphics (PNG) is a raster graphics file format that supports lossless data compression.

也就是说,png是一种使用无损压缩的图片格式,而大家熟知的另外一种图片格式——jpeg则是采用有损压缩的方式。用通俗易懂的方式来讲,当原图片数据被编码成png格式后,是可以完全还原成原本的图片数据的,而编码成jpeg则会损耗一部分图片数据,这是因为两者的编码方式和定位不同。jpeg着重于人眼的观感,保留更多的亮度信息,去掉一些不影响观感的色度信息,因此是有损耗的压缩。png则保留原始所有的颜色信息,并且支持透明/alpha通道,然后采用无损压缩进行编码。因此对于jpeg来说,通常适合颜色更丰富、可以在人眼识别不了的情况下尽可能去掉冗余颜色数据的图片,比如照片之类的图片;而png适合需要保留原始图片信息、需要支持透明度的图片。

以下,我们来尝试获取png编码的图片数据:

 金沙棋牌官方平台 1

优劣

使用隔行扫描有什么好处呢?如果大家有去仔细观察的话,会发现网络上有一些png图在加载时可以做到先显示出比较模糊的图片,然后逐渐越来越清晰,最后显示出完整的图片,类似如下效果:金沙棋牌官方平台 2

这就是隔行扫描能带来的效果。隔行扫描一共会进行1到7次扫描,每一次都是跳着部分像素点进行扫描的,先扫描到像素点可以先渲染,每多一次扫描,图片就会更清晰,到最后一次扫描时就会扫描完所有像素点,进而渲染出完整的图片。

当然,也因为要进行跳像素扫描,整张图片会存储更多额外数据而导致图片大小会稍微变大,具体增加了什么额外数据下文会进行讲解。

m_dataSize = din.readInt(); // 像素数据大小(压缩数据)
m_data = new byte[m_dataSize];
din.read(m_data); // 读取像素数据(压缩数据)
// 读取每个图块数据的起始偏移量
int offset = 0;
m_dataOffset = new int[m_modelCount];
for (int i = 0; i < m_modelCount; i++) {
m_dataOffset[i] = offset;
if ((m_flags & FLAG_16BIT_4_LEN) != 0) {
offset += din.readShort();
} else {
offset += din.readByte() & 0xff;
}
}
} catch (Exception ex) {
}
}

结构

图片是属于2进制文件,因此在拿到png图片并想对其进行解析的话,就得以二进制的方式进行读取操作。png图片包含两部分:文件头和数据块。

        其中第一个字节0x89超出了ASCII字符的范围,这是为了避免某些软件将PNG文件当做文本文件来处理。文件中剩余的部分由3个以上的PNG的数据块(Chunk)按照特定的顺序组成,因此,一个标准的PNG文件结构应该如下:

生成

要导出一张基于Adam7隔行扫描的png图片是非常简单,我们可以借助Adobe的神器——PhotoShop(以下简称ps)。我们把一张普通的图片拖入到ps中,然后依次点选【文件】-【存储为Web所用的格式】,在弹出的框里选择存储为PNG-24,然后勾选交错,最后点击存储即可。

这里的交错就是只将扫描算法设为Adam7隔行扫描,如果不勾选交错,则是普通逐行扫描的png图片。

* 解压缩指定图块像素数据
*
* @param model_id
* int 图块号
* @param pal_id
* int 调色板号
* @return int[] 解压缩图块像素数据(ARPG值)
**************************************************************************/
private int[] BuildRle8bFrm(int model_id, int pal_id) {
// 计算解压后,像素数据的大小(图块W*图块H)
int size;
if ((m_flags & FLAG_REBUILD_SIZE) != 0) {
size = m_rebuildWidth * m_rebuildHeight;
} else {
size = (m_models[model_id * 2] & 0xff)
* (m_models[model_id * 2 + 1] & 0xff);
}
// 初始化像素buf
int[] m_bufB = new int[size];
int pal[] = m_pal[pal_id]; // 获取当前调色板
int offset = m_dataOffset[model_id]; // 获取压缩数据起点
// 解压缩
int count, index, pos = 0;
while (pos < size) {
count = ((m_data[offset] & 0xFF) >> m_count) + 1;
index = pal[m_data[offset] & m_mask];
offset++;
while (--count >= 0) {
m_bufB[pos++] = index;
}
}
return m_bufB;
}

文件头

png的文件头就是png图片的前8个字节,其值为[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],人们常常把这个头称之为“魔数”。玩过linux的同学估计知道,可以使用file命令类判断一个文件是属于格式类型,就算我们把这个文件类型的后缀改得乱七八糟也可以识别出来,用的就是判断“魔数”这个方法。有兴趣的同学还可以使用String.fromCharCode将这个“魔数”转成字符串看看,就知道为什么png会取这个值作为文件头了。

用代码来判断也很简单:

JavaScript

// 读取指定长度字节 function readBytes(buffer, begin, length) {     return Array.prototype.slice.call(buffer, begin, begin + length); }   let header = readBytes(pngBuffer, 0, 8); // [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]

1
2
3
4
5
6
// 读取指定长度字节
function readBytes(buffer, begin, length) {
    return Array.prototype.slice.call(buffer, begin, begin + length);
}
 
let header = readBytes(pngBuffer, 0, 8); // [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]

PNG文件标志

PNG数据块

……

PNG数据块

原理

Adam7隔行扫描算法的原理并不难,本质上是将一张png图片拆分成多张png小图,然后对这几张png小图进行普通的逐行扫描解析,最后将解析出来的像素数据按照一定的规则进行归位即可。

* 获取指定图块Image
*
* @param model_id
* int 图块号
* @param pal_id
* int 调色板号
* @return Image 图块Image对象
**************************************************************************/
public Image GetImage(int model_id, int pal_id) {
// 获得指定图块解压数据(ARPG颜色数据)
int[] m_bufB = BuildRle8bFrm(model_id, pal_id);
// 计算图块尺寸
int w, h;
if ((m_flags & FLAG_REBUILD_SIZE) != 0) {
w = m_rebuildWidth;
h = m_rebuildHeight;
} else {
w = m_models[model_id * 2] & 0xff;
h = m_models[model_id * 2 + 1] & 0xff;
}
// 生成Image图片
Image m_image = Image.createRGBImage(m_bufB, w, h, true);
m_bufB = null;
return m_image;
}
}

数据块

去掉了png图片等前8个字节,剩下的就是存放png数据的数据块,我们通常称之为chunk

顾名思义,数据块就是一段数据,我们按照一定规则对png图片(这里指的是去掉了头的png图片数据,下同)进行切分,其中一段数据就是一个数据块。每个数据块的长度是不定的,我们需要通过一定的方法去提取出来,不过我们要先知道有哪些类型的数据块才好判断。

PNG数据块(Chunk)

        PNG定义了两种类型的数据块,一种是称为关键数据块(critical chunk),这是标准的数据块,另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。关键数据块定义了4个标准数据块,每个PNG文件都必须包含它们,PNG读写软件也都必须要支持这些数据块。你可以从“可选否”一栏查看是否必须支持的数据块。虽然PNG文件规范没有要求PNG编译码器对可选数据块进行编码和译码,但规范提倡支持可选数据块。

下表就是PNG中数据块的类别,其中,关键数据块部分我们使用深色背景加以区分。

PNG文件格式中的数据块

数据块符号

数据块名称 

多数据块 

可选否 

位置限制 

IHDR 

文件头数据块 

否 

否 

第一块 

cHRM 

基色和白色点数据块 

否 

在PLTE和IDAT之前

gAMA 

图像γ数据块 

否 

在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 

压缩文本数据块 

无限制 

fRAc 

(专用公共数据块) 

无限制 

gIFg 

(专用公共数据块) 

无限制 

gIFt 

(专用公共数据块) 

无限制 

gIFx 

(专用公共数据块) 

无限制 

IEND 

图像结束数据 

否 

否 

最后一个数据块 

这里要补充一个iCCP

分析

在解压缩完图像数据后就要马上进行拆图。拆图并不难,就是将原本存储图像数据的Buffer数组拆分成多个Buffer数组而已。关键的问题是怎么拆,这时我们先祭上wiki上这张图:

金沙棋牌官方平台 3

上面这张图就说明了每次扫描需要扫描到的像素,正常来说一张基于Adam7隔行扫描的png图片是要经历7次扫描的,不过有些比较小的图片的实际扫描次数不到7次,这是因为有些扫描因为没有实际像素点而落空的原因,所以下面的讲解还是以标准的7次扫描来讲解,本质上此算法的代码写出来后,是能兼容任何大小的png图片的,因为算法本身和图片大小无关。

7次扫描,其实就回答了上面拆图的问题:要拆成7张小图。每张小图就包含了每次扫描时要归位的像素点。

以第一次扫描为例:第一次扫描的规则是从左上角(我们设定此坐标为(0,0))开始,那么它扫描到的下一个点是同一行上一个点往右偏移8个像素,即(8,0)。以此类推,再下一个点就是(16,0)、(24,0)等。当当前行所有符合规则的点都扫描完时则跳到下一个扫描行的起点,即(8,0),也就是说第一次扫描的扫描行也是以8个像素为偏移单位的。直到所有扫描行都已经扫描完成,我们就可以认为这次扫描已经结束,可以考虑进入第二次扫描。

我们以一张10*10大小的png图片来举例,下面每个数字代表一个像素点,数字的值代表这个点在第几次扫描时被扫描到:

JavaScript

1 6 4 6 2 6 4 6 1 6 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 3 6 4 6 3 6 4 6 3 6 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 1 6 4 6 2 6 4 6 1 6 7 7 7 7 7 7 7 7 7 7

1
2
3
4
5
6
7
8
9
10
1 6 4 6 2 6 4 6 1 6
7 7 7 7 7 7 7 7 7 7
5 6 5 6 5 6 5 6 5 6
7 7 7 7 7 7 7 7 7 7
3 6 4 6 3 6 4 6 3 6
7 7 7 7 7 7 7 7 7 7
5 6 5 6 5 6 5 6 5 6
7 7 7 7 7 7 7 7 7 7
1 6 4 6 2 6 4 6 1 6
7 7 7 7 7 7 7 7 7 7

按照规则,在第一次扫描时我们会扫描到4个像素点,我们把这4个像素点单独抽离出来合在一起,就是我们要拆的第一张小图:

JavaScript

(1) 6 4 6 2 6 4 6 (1) 6 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 1 1 3 6 4 6 3 6 4 6 3 6 ==> 1 1 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 (1) 6 4 6 2 6 4 6 (1) 6 7 7 7 7 7 7 7 7 7 7

1
2
3
4
5
6
7
8
9
10
(1)  6   4   6   2   6   4   6  (1)  6
7   7   7   7   7   7   7   7   7   7
5   6   5   6   5   6   5   6   5   6
7   7   7   7   7   7   7   7   7   7                   1 1
3   6   4   6   3   6   4   6   3   6        ==>        1 1
7   7   7   7   7   7   7   7   7   7
5   6   5   6   5   6   5   6   5   6
7   7   7   7   7   7   7   7   7   7
(1)  6   4   6   2   6   4   6  (1)  6
7   7   7   7   7   7   7   7   7   7

也就是说,我们的第一张小图就是2*2大小的png图片。后面的小图大小以此类推,这样我们就能得知拆图的依据了。

数据块类型

数据块类型有很多种,但是其中大部分我们都不需要用到,因为里面没有存储我们需要用到的数据。我们需要关注的数据块只有以下四种:

  • IHDR:存放图片信息。
  • PLTE:存放索引颜色。
  • IDAT:存放图片数据。
  • IEND:图片数据结束标志。

只要解析这四种数据块就可以获取图片本身的所有数据,因此我们也称这四种数据块为“关键数据块”

数据块结构

PNG文件中,每个数据块(比如IHDR,cHRM,IDAT等)由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域中的数据进行计算得到的。CRC具体算法定义在ISO 3309和ITU-T V.42中.

注意:Length值的是除:length本身,Chunk Type Code,CRC外的长度,也就是Chunk Data的长度。

下面,我们依次来了解一下各个【关键数据块】的结构

拆图

上面有提到,拆图本质上就是把存放图片数据的Buffer数组进行切分,在nodejs里的Buffer对象有个很好用的方法——slice,它的用法和数组的同名方法一样。

直接用上面的例子,我们的第一张小图是2*2点png图片,在假设我们一个像素点所占的字节数是3个,那么我们要切出来的第一个Buffer子数组的长度就是2*(2*3+1)。也许就有人好奇了,为什么是乘以2*3+1而不是直接乘以2*3呢?之前我们提到过,拆成小图后要对小图进行普通的逐行扫描解析,这样解析的话每一行的第一个字节实际存放的不是图像数据,而是过滤类型,因此每一行所占用的字节需要在2*3的基础上加1。

数据块格式

数据块格式如下:

描述 长度
数据块内容长度 4字节
数据块类型 4字节
数据块内容 不定字节
crc冗余校验码 4字节

这样我们就可以轻易的指导当前数据块的长度了,即数据块内容长度 + 12字节,用代码实现如下:

JavaScript

// 读取32位无符号整型数 function readInt32(buffer, offset) {     offset = offset || 0;     return (buffer[offset] << 24) + (buffer[offset + 1] << 16) + (buffer[offset + 2] << 8) + (buffer[offset + 3] << 0); }   let length = readInt32(readBytes(4)); // 数据块内容长度 let type = readBytes(4); // 数据块类型 let chunkData = readBytes(length); // 数据块内容 let crc = readBytes(4); // crc冗余校验码

1
2
3
4
5
6
7
8
9
10
// 读取32位无符号整型数
function readInt32(buffer, offset) {
    offset = offset || 0;
    return (buffer[offset] << 24) + (buffer[offset + 1] << 16) + (buffer[offset + 2] << 8) + (buffer[offset + 3] << 0);
}
 
let length = readInt32(readBytes(4)); // 数据块内容长度
let type = readBytes(4); // 数据块类型
let chunkData = readBytes(length); // 数据块内容
let crc = readBytes(4); // crc冗余校验码

这里的crc冗余校验码在我们解码过程中用不到,所以这里不做详解。除此之外,数据块内容长度和数据块内容好解释,不过数据块类型有何作用呢,这里我们先将这个type转成字符串类型:

JavaScript

// 将buffer数组转为字符串 function bufferToString(buffer) {     let str = '';     for(let i=0, len=buffer.length; i<len; i++){         str += String.fromCharCode(buffer[i]);     }     return str; }   type = bufferToString(type);

1
2
3
4
5
6
7
8
9
10
// 将buffer数组转为字符串
function bufferToString(buffer) {
    let str = '';
    for(let i=0, len=buffer.length; i<len; i++){
        str += String.fromCharCode(buffer[i]);
    }
    return str;
}
 
type = bufferToString(type);

然后会发现type的值是四个大写英文字母,没错,这就是上面提到的数据块类型。上面还提到了我们只需要解析关键数据块,因此遇到type不等于IHDR、PLTE、IDAT、IEND中任意一个的数据块就直接舍弃好了。当我们拿到一个关键数据块,就直接解析其数据块内容就可以了,即上面代码中的chunkData字段。

IHDR

        文件头数据块IHDR(header chunk):它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流(文件)中只能有一个文件头数据块。
文件头数据块由13字节组成,它的格式如下表所示:

域的名称 

字节数 

说明 

Width 

4 bytes 

图像宽度,以像素为单位 

Height 

4 bytes 

图像高度,以像素为单位 

Bit depth 

1 byte 

图像深度: 
索引彩色图像:1,2,4或8 
灰度图像:1,2,4,8或16 
真彩色图像:8或16 

ColorType 

1 byte 

颜色类型:
0:灰度图像, 1,2,4,8或16 
2:真彩色图像,8或16 
3:索引彩色图像,1,2,4或8 
4:带α通道数据的灰度图像,8或16 
6:带α通道数据的真彩色图像,8或16 

Compression method 

1 byte 

压缩方法(LZ77派生算法) 

Filter method 

1 byte 

滤波器方法 

Interlace method 

1 byte 

隔行扫描方法:
0:非隔行扫描 
1: Adam7(由Adam M. Costello开发的7遍隔行扫描方法) 

 

由于我们研究的是手机上的PNG,因此,首先我们看看MIDP1.0对所使用PNG图片的要求吧:

● 在MIDP1.0中,我们只可以使用1.0版本的PNG图片。并且,所以的PNG关键数据块都有特别要求:
IHDR
● 文件大小:MIDP支持任意大小的PNG图片,然而,实际上,如果一个图片过大,会由于内存耗尽而无法读取。
● 颜色类型:所有颜色类型都有被支持,虽然这些颜色的显示依赖于实际设备的显示能力。同时,MIDP也能支持alpha通道,但是,所有的alpha通道信息都会被忽略并且当作不透明的颜色对待。
● 色深:所有的色深都能被支持。
● 压缩方法:仅支持压缩方式0(deflate压缩方式),这和jar文件的压缩方式完全相同,所以,PNG图片数据的解压和jar文件的解压可以使用相同的代码。(其实这也就是为什么J2ME能很好的支持PNG图像的原因:))
● 滤波器方法:尽管在PNG的白皮书中仅定义了方法0,然而所有的5种方法都被支持!
● 隔行扫描:虽然MIDP支持0、1两种方式,然而,当使用隔行扫描时,MIDP却不会真正的使用隔行扫描方式来显示。
● PLTE chunk:支持
● IDAT chunk:图像信息必须使用5种过滤方式中的方式0 (None, Sub, Up, Average, Paeth)
● IEND chunk:当IEND数据块被找到时,这个PNG图像才认为是合法的PNG图像。
● 可选数据块:MIDP可以支持下列辅助数据块,然而,这却不是必须的。
bKGD cHRM gAMA hIST iCCP iTXt pHYs
sBIT sPLT sRGB tEXt tIME tRNS zTXt

关于更多的信息,可以参考www.w3.org

像素归位

其他的小图拆分的方法是一样,在最后一次扫描完毕后,我们就会拿到7张小图。然后我们按照上面的规则对这些小图的像素进行归位,也就是填回去的意思。下面简单演示下归位的流程:``

JavaScript

(1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) 1 1 ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) 1 1 ==> ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )

1
2
3
4
5
6
7
8
9
10
                  (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
1 1              ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
1 1     ==>      ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
                  (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )

待到7张小图的像素全部都归位后,最后我们就能拿到一张完整的png图片了。

IHDR

类型为IHDR的数据块用来存放图片信息,其长度为固定的13个字节:

描述 长度
图片宽度 4字节
图片高度 4字节
图像深度 1字节
颜色类型 1字节
压缩方法 1字节
过滤方式 1字节
扫描方式 1字节

其中宽高很好解释,直接转成32位整数,就是这张png图片等宽高(以像素为单位)。压缩方法目前只支持一种(deflate/inflate 压缩算法),其值为0;过滤方式也只有一种(包含标准的5种过滤类型),其值为0;扫描方式有两种,一种是逐行扫描,值为0,还有一种是Adam7隔行扫描,其值为1,此次只针对普通的逐行扫描方式进行解析,因此暂时不考虑Adam7隔行扫描。

图片深度是指每个像素点中的每个通道(channel)占用的位数,只有1、2、4、8和16这5个值;颜色类型用来判断每个像素点中有多少个通道,只有0、2、3、4和6这5个值:

颜色类型的值 占用通道数 描述
0 1 灰度图像,只有1个灰色通道
2 3 rgb真彩色图像,有RGB3色通道
3 1 索引颜色图像,只有索引值一个通道
4 2 灰度图像 + alpha通道

pHYs

物理像素数据块,它表示了图片的像素尺寸,或者是高宽比,它的结果如下

Pixels per unit, X axis

4 bytes (PNG unsigned integer)

Pixels per unit, Y axis

4 bytes (PNG unsigned integer)

Unit specifier

1 byte

unit specifier的定义如下:

0

unit is unknown

1

unit is the metre

 

 

代码

整个流程的代码如下:

JavaScript

let width; // 完整图像宽度,解析IHDR数据块可得 let height; // 完整图像高度,解析IHDR数据块可得 let colors; // 通道数,解析IHDR数据块可得 let bitDepth; // 图像深度,解析IHDR数据块可得 let data; // 完整图像数据 let bytesPerPixel = Math.max(1, colors * bitDepth / 8); // 每像素字节数 let pixelsBuffer = Buffer.alloc(bytesPerPixel * width * height, 0xFF); // 用来存放最后解析出来的图像数据 // 7次扫描的规则 let startX = [0, 0, 4, 0, 2, 0, 1]; let incX = [8, 8, 8, 4, 4, 2, 2]; let startY = [0, 4, 0, 2, 0, 1, 0]; let incY = [8, 8, 4, 4, 2, 2, 1]; let offset = 0; // 记录小图开始位置 // 7次扫描 for(let i=0; i<7; i++) { // 子图像信息 let subWidth = Math.ceil((width - startY[i]) / incY[i], 10); // 小图宽度 let subHeight = Math.ceil((height - startX[i]) / incX[i], 10); // 小图高度 let subBytesPerRow = bytesPerPixel * subWidth; // 小图每行字节数 let offsetEnd = offset + (subBytesPerRow + 1) * subHeight; // 小图结束位置 let subData = data.slice(offset, offsetEnd); // 小图像素数据 // 对小图进行普通的逐行扫描 let subPixelsBuffer = this.interlaceNone(subData, subWidth, subHeight, bytesPerPixel, subBytesPerRow); let subOffset = 0; // 像素归位 for(let x=startX[i]; x<height; x+=incX[i]) { for(let y=startY[i]; y<width; y+=incY[i]) { // 逐个像素拷贝回原本所在的位置 for(let z=0; z<bytesPerPixel; z++) { pixelsBuffer[(x * width + y) * bytesPerPixel + z] = subPixelsBuffer[subOffset++] & 0xFF; } } } offset = offsetEnd; // 置为下一张小图的开始位置 } return pixelsBuffer;

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
let width; // 完整图像宽度,解析IHDR数据块可得
let height; // 完整图像高度,解析IHDR数据块可得
let colors; // 通道数,解析IHDR数据块可得
let bitDepth; // 图像深度,解析IHDR数据块可得
let data; // 完整图像数据
let bytesPerPixel = Math.max(1, colors * bitDepth / 8); // 每像素字节数
let pixelsBuffer = Buffer.alloc(bytesPerPixel * width * height, 0xFF); // 用来存放最后解析出来的图像数据
// 7次扫描的规则
let startX = [0, 0, 4, 0, 2, 0, 1];
let incX = [8, 8, 8, 4, 4, 2, 2];
let startY = [0, 4, 0, 2, 0, 1, 0];
let incY = [8, 8, 4, 4, 2, 2, 1];
let offset = 0; // 记录小图开始位置
// 7次扫描
for(let i=0; i<7; i++) {
    // 子图像信息
    let subWidth = Math.ceil((width - startY[i]) / incY[i], 10); // 小图宽度
    let subHeight = Math.ceil((height - startX[i]) / incX[i], 10); // 小图高度
    let subBytesPerRow = bytesPerPixel * subWidth; // 小图每行字节数
    let offsetEnd = offset + (subBytesPerRow + 1) * subHeight; // 小图结束位置
    let subData = data.slice(offset, offsetEnd); // 小图像素数据
    // 对小图进行普通的逐行扫描
    let subPixelsBuffer = this.interlaceNone(subData, subWidth, subHeight, bytesPerPixel, subBytesPerRow);
    let subOffset = 0;
    // 像素归位
    for(let x=startX[i]; x<height; x+=incX[i]) {
        for(let y=startY[i]; y<width; y+=incY[i]) {
            // 逐个像素拷贝回原本所在的位置
            for(let z=0; z<bytesPerPixel; z++) {
                pixelsBuffer[(x * width + y) * bytesPerPixel + z] = subPixelsBuffer[subOffset++] & 0xFF;
            }
        }
    }
    offset = offsetEnd; // 置为下一张小图的开始位置
}
return pixelsBuffer;

PLTE

类型为PLTE的数据块用来存放索引颜色,我们又称之为“调色板”。

由IHDR数据块解析出来的图像信息可知,图像的数据可能是以索引值的方式进行存储。当图片数据采用索引值的时候,调色板就起作用了。调色板的长度和图像深度有关,假设图像深度的值是x,则其长度通常为2的x次幂 * 3。原因是图像深度保存的就是通道占用的位数,而在使用索引颜色的时候,通道里存放的就是索引值,2点x次幂就表示这个通道可能存放的索引值有多少个,即调色板里的颜色数。而每个索引颜色是RGB3色通道存放的,因此此处还需要乘以3。

通常使用索引颜色的情况下,图像深度的值即为8,因而调色板里存放的颜色就只有256种颜色,长度为256 * 3个字节。再加上1位布尔值表示透明像素,这就是我们常说的png8图片了。

PLTE

调色板数据块PLTE(palette chunk)包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。
PLTE数据块是定义图像的调色板信息,PLTE可以包含1~256个调色板信息,每一个调色板信息由3个字节组成:

颜色

字节

意义

Red

1 byte

0 = 黑色, 255 = 红

Green

1 byte

0 = 黑色, 255 = 绿色

Blue

1 byte

0 = 黑色, 255 = 蓝色 

 

因此,调色板的长度应该是3的倍数,否则,这将是一个非法的调色板。
对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。
真彩色图像和带α通道数据的真彩色图像也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。

尾声

整个Adam7隔行扫描的流程大概就是这样:

金沙棋牌官方平台 4

 

1 赞 2 收藏 评论

金沙棋牌官方平台 5

IDAT

类型为IDAT的数据块用来存放图像数据,跟其他关键数据块不同的是,其数量可以是连续的复数个;其他关键数据块在1个png文件里有且只有1个。

这里的数据得按顺序把所有连续的IDAT数据块全部解析并将数据联合起来才能进行最终处理,这里先略过。

JavaScript

let dataChunks = []; let length = 0; // 总数据长度   // ...   while(/* 存在IDAT数据块 */) {     dataChunks.push(chunkData);     length += chunkData.length; }

1
2
3
4
5
6
7
8
9
let dataChunks = [];
let length = 0; // 总数据长度
 
// ...
 
while(/* 存在IDAT数据块 */) {
    dataChunks.push(chunkData);
    length += chunkData.length;
}

IDAT

图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。
IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构,我们就可以很方便的生成PNG图像。

IEND

当解析到类型为IEND的数据块时,就表明所有的IDAT数据块已经解析完毕,我们就可以停止解析了。

IEND整个数据块的值时固定的:[0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82],因为IEND数据块没有数据块内容,所以其数据块内容长度字段(数据块前4个字节)的值也是0。

IEND

图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。
如果我们仔细观察PNG文件,我们会发现,文件的结尾12个字符看起来总应该是这样的:00 00 00 00 49 45 4E 44 AE 42 60 82 

金沙棋牌官方平台 6
不难理解,由于数据块结构的定义,IEND数据块的长度总是0(00 00 00 00,除非人为加入信息),数据标识总是IEND(49 45 4E 44),因此,CRC码也总是AE 42 60 82。

IHDR cHRM pHYs IEND

 金沙棋牌官方平台 7

金沙棋牌官方平台 8

解析

解压缩

当我们收集完IDAT的所有数据块内容时,我们要先对其进行解压缩:

JavaScript

const zlib = require('zlib');   let data = new Buffer(length); let index = 0; dataChunks.forEach((chunkData) => {     chunkData.forEach((item) => {data[index++] = item}); });   // inflate解压缩 data = zlib.inflateSync(new Buffer(data));

1
2
3
4
5
6
7
8
9
10
const zlib = require('zlib');
 
let data = new Buffer(length);
let index = 0;
dataChunks.forEach((chunkData) => {
    chunkData.forEach((item) => {data[index++] = item});
});
 
// inflate解压缩
data = zlib.inflateSync(new Buffer(data));

扫描

上面说过,此次我们只考虑逐行扫描的方式:

JavaScript

// 读取8位无符号整型数 function readInt8(buffer, offset) {     offset = offset || 0;     return buffer[offset] << 0; }   let width; // 解析IHDR数据块时得到的图像宽度 let height; // 解析IHDR数据块时得到的图像高度 let colors; // 解析IHDR数据块时得到的通道数 let bitDepth; // 解析IHDR数据块时得到的图像深度   let bytesPerPixel = Math.max(1, colors * bitDepth / 8); // 每像素字节数 let bytesPerRow = bytesPerPixel * width; // 每行字节数   let pixelsBuffer = new Buffer(bytesPerPixel * width * height); // 存储过滤后的像素数据 let offset = 0; // 当前行的偏移位置   // 逐行扫描解析 for(let i=0, len=data.length; i<len; i+=bytesPerRow+1) {     let scanline = Array.prototype.slice.call(data, i+1, i+1+bytesPerRow); // 当前行     let args = [scanline, bytesPerPixel, bytesPerRow, offset];       // 第一个字节代表过滤类型     switch(readInt8(data, i)) {         case 0:             filterNone(args);             break;         case 1:             filterSub(args);             break;         case 2:             filterUp(args);             break;         case 3:             filterAverage(args);             break;         case 4:             filterPaeth(args);             break;         default:             throw new Error('未知过滤类型!');     }       offset += bytesPerRow; }

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
// 读取8位无符号整型数
function readInt8(buffer, offset) {
    offset = offset || 0;
    return buffer[offset] << 0;
}
 
let width; // 解析IHDR数据块时得到的图像宽度
let height; // 解析IHDR数据块时得到的图像高度
let colors; // 解析IHDR数据块时得到的通道数
let bitDepth; // 解析IHDR数据块时得到的图像深度
 
let bytesPerPixel = Math.max(1, colors * bitDepth / 8); // 每像素字节数
let bytesPerRow = bytesPerPixel * width; // 每行字节数
 
let pixelsBuffer = new Buffer(bytesPerPixel * width * height); // 存储过滤后的像素数据
let offset = 0; // 当前行的偏移位置
 
// 逐行扫描解析
for(let i=0, len=data.length; i<len; i+=bytesPerRow+1) {
    let scanline = Array.prototype.slice.call(data, i+1, i+1+bytesPerRow); // 当前行
    let args = [scanline, bytesPerPixel, bytesPerRow, offset];
 
    // 第一个字节代表过滤类型
    switch(readInt8(data, i)) {
        case 0:
            filterNone(args);
            break;
        case 1:
            filterSub(args);
            break;
        case 2:
            filterUp(args);
            break;
        case 3:
            filterAverage(args);
            break;
        case 4:
            filterPaeth(args);
            break;
        default:
            throw new Error('未知过滤类型!');
    }
 
    offset += bytesPerRow;
}

上面代码前半部分不难理解,就是通过之前解析得到的图像宽高,再加上图像深度和通道数计算得出每个像素占用的字节数和每一行数据占用的字节数。因此我们就可以拆分出每一行的数据和每一个像素的数据。

在得到每一行数据后,就要进行这个png编码里最关键的1步——过滤。

过滤

早先我们说过过滤方法只有1种,其中包含5种过滤类型,图像每一行数据里的第一个字节就表示当前行数什么过滤类型。

png为什么要对图像数据进行过滤呢?

大多数情况下,图像的相邻像素点的色值时很相近的,而且很容易呈现线性变化(相邻数据的值是相似或有某种规律变化的),因此借由这个特性对图像的数据进行一定程度的压缩。针对这种情况我们常常使用一种叫差分编码的编码方式,即是记录当前数据和某个标准值的差距来存储当前数据。

比如说有这么一个数组[99, 100, 100, 102, 103],我们可以将其转存为[99, 1, 0, 2, 1]。转存的规则就是以数组第1位为标准值,标准值存储原始数据,后续均存储以前1位数据的差值。

当我们使用了差分编码后,再进行deflate压缩的话,效果会更好(deflate压缩是LZ77延伸出来的一种算法,压缩频繁重复出现的数据段的效果是相当不错的,有兴趣的同学可自行去了解)。

好,回到正题来讲png的5种过滤类型,首先我们要定义几个变量以便于说明:

JavaScript

C B A X

1
2
C B
A X

本文由金沙棋牌发布于金沙棋牌官方平台,转载请注明出处:图片压缩算法,获取图片信息和像素内容

关键词:

上一篇:深入理解CSS3

下一篇:没有了