我正在参加「掘金·启航计划」
分享背景
我们经常使用图片来表达信息,图片已然成为一种沟通方式。图片可以表达信息,我们也需要知道图片本身是如何被表达出来的。鲁迅说过知其然不如知其所以然,本文将就图片的类型和常见图片格式的二进制进行解析,希望大家看完会对图片有一个更系统的认识。
文中的源码示例,使用Dart语音编写,不过不了解Flutter的同学,也不影响阅读。
图片分类
光栅图(栅格图)
就是最小单位由像素构成的图,只有点的信息,缩放时会失真。JPG、PNG,webp等都属于此类。
为了更好说明,写了一个Demo,来更加形象的说明什么是栅格图。 思路很简单,就是使用image库来获取图片的宽高和每一个像素点的色值,然后使用Column,Row,ColoredBox三个组件显示出来。
附源码
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image/image.dart' as img;
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('栅格图片'),
),
body: HomePage(),
));
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
img.Image? image;
@override
void initState() {
super.initState();
loadImageFromAssets('assets/image/leaf_152.JPG').then((value) {
setState(() {
image = value;
});
});
}
//读取 assets 中的图片
Future<img.Image?> loadImageFromAssets(String path) async {
ByteData data = await rootBundle.load(path);
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
return img.decodeImage(bytes);
}
@override
Widget build(BuildContext context) {
if (image != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('76 * 76'),
Container(
width: 76,
height: 76,
decoration: BoxDecoration(
border: Border.all(color: Colors.amber, width: 1)),
child: GridXYLayout(
n: image!.width,
m: image!.height,
image: image!,
),
),
Text('400 * 400'),
Container(
width: 400,
height: 400,
decoration: BoxDecoration(
border: Border.all(color: Colors.amber, width: 1)),
child: GridXYLayout(
n: image!.width,
m: image!.height,
image: image!,
),
),
],
);
}
return const SizedBox.shrink();
}
}
class GridXYLayout extends StatelessWidget {
img.Image image;
// 行数
final int n;
// 列数
final int m;
GridXYLayout({Key? key, this.n = 3, this.m = 3, required this.image})
: super(key: key);
Widget get decoratedBox {
return DecoratedBox(
decoration:
BoxDecoration(border: Border.all(color: randomColor(), width: 1)),
);
}
@override
Widget build(BuildContext context) {
List<Widget> children = [];
for (int i = 0; i < n; i++) {
List<Widget> columnChildren = [];
for (int j = 0; j < m; j++) {
columnChildren.add(
Expanded(
child: buildZone(j, i),
),
);
}
children.add(Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: columnChildren,
)));
}
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
);
}
Widget buildZone(int x, int y) {
//if (x % 2 != 0 && y % 2 != 0) {
//显示点图
if (true) {
Color color = Color(image.getPixel(y, x));
color = Color.fromARGB(
color.alpha,
color.blue,
color.green,
color.red,
);
// return ColorFiltered(
// colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.color),
// child: ColoredBox(color: color));
return ColoredBox(color: color);
} else {
return const SizedBox.shrink();
}
}
// 随机色
final Random random = Random();
Color randomColor({
int limitA = 120,
int limitR = 0,
int limitG = 0,
int limitB = 0,
}) {
int a = limitA + random.nextInt(256 - limitA); //透明度值
int r = limitR + random.nextInt(256 - limitR); //红值
int g = limitG + random.nextInt(256 - limitG); //绿值
int b = limitB + random.nextInt(256 - limitB); //蓝值
return Color.fromARGB(a, r, g, b); //生成argb模式的颜色
}
}
矢量图片
使用点,线和多边形等几何形状来构图,通过数据计算并且绘制而成。具有高分辨率和缩放功能。SVG就是一种矢量图。
还是一样,为了更直观的说明什么时候矢量图,我们还是写一个Demo来表达。使用path_drawing库,这个库可以基于矢量图的路径进行解析。然后把解析出来的路径path使用自定义CustomPainter绘制出来。
| 原图 | 路径解析绘制 |
|---|---|
了解知识,svg 图片格式中的path指令
M/m (x,y)+ 移动当前位置
L/l (x,y)+ 直线
H/h (x)+ 水平线
V/v (x)+ 竖直线
Z/z 闭合路径
Q/q (x1,y1,x,y)+ 二次贝塞尔曲线
T/t (x,y)+ 光滑绘制二次贝塞尔曲线
C/c (x1,y1,x2,y2,x,y)+ 三次贝塞尔曲线
S/s (x2,y2,x,y)+ 光滑绘制三次贝塞尔曲线
A/a (rx,ry,xr,laf,sf,x,y) 弧线
每个指令都有 大写字母 和 小写字母。
其中 大写字母 表示其后的坐标是 绝对坐标 ,也就是以区域 左上角 为原点的坐标。
附源码
import 'package:flutter/material.dart';
import 'package:path_drawing/path_drawing.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('矢量图片'),
),
body: const VectorImageTest(),
));
}
}
class VectorImageTest extends StatefulWidget {
const VectorImageTest({Key? key}) : super(key: key);
@override
State<VectorImageTest> createState() => _VectorImageTestState();
}
class _VectorImageTestState extends State<VectorImageTest> {
final trianglePath = parseSvgPathData(
'M19.4889505,0 C20.7568042,-2.32900945e-16 21.7846027,1.02779849 21.7846027,2.29565218 L21.7846027,25.2043478 C21.7846027,26.4722015 20.7568042,27.5 19.4889505,27.5 C18.2210968,27.5 17.1932983,26.4722015 17.1932983,25.2043478 L17.1929268,22.917 L10.3059268,22.917 L10.3063312,25.2043478 C10.3063312,26.4722015 9.27853273,27.5 8.01067904,27.5 C6.74282535,27.5 5.71502686,26.4722015 5.71502686,25.2043478 L5.71502686,2.29565218 C5.71502686,1.02779849 6.74282535,2.32900945e-16 8.01067904,0 C9.27853273,-2.32900945e-16 10.3063312,1.02779849 10.3063312,2.29565218 L10.3059268,4.583 L17.1929268,4.583 L17.1932983,2.29565218 C17.1932983,1.02779849 18.2210968,2.32900945e-16 19.4889505,0 Z M4.56731796,18.3337402 L4.56731796,22.9170737 L2.84159347,22.9170737 C1.5759409,22.9170737 0.549926758,21.8910595 0.549926758,20.625407 C0.549926758,19.3597544 1.5759409,18.3337402 2.84159347,18.3337402 L4.56731796,18.3337402 Z M24.6584065,18.3337402 C25.9240591,18.3337402 26.9500732,19.3597544 26.9500732,20.625407 C26.9500732,21.8910595 25.9240591,22.9170737 24.6584065,22.9170737 L24.6584065,22.9170737 L22.932682,22.9170737 L22.932682,18.3337402 Z M17.7674484,14.0368652 L9.73266602,14.0368652 L9.73266602,14.6028075 C9.73266602,16.8215514 11.5313133,18.6201987 13.7500572,18.6201987 C15.9688011,18.6201987 17.7674484,16.8215514 17.7674484,14.6028075 L17.7674484,14.6028075 L17.7674484,14.0368652 Z M10.5959038,8.88061523 C9.79988922,8.88061523 9.15863037,9.52187408 9.15863037,10.312907 C9.15863037,11.1039398 9.79988922,11.7451987 10.5909221,11.7451987 L10.5909221,11.7451987 C11.3869367,11.7451987 12.0281956,11.1039398 12.0281956,10.312907 C12.0281956,9.52187408 11.3869367,8.88061523 10.5959038,8.88061523 L10.5959038,8.88061523 Z M16.9090752,8.88061523 C16.1130606,8.88061523 15.4718018,9.52187408 15.4718018,10.312907 C15.4718018,11.1039398 16.1130606,11.7451987 16.9040935,11.7451987 L16.9040935,11.7451987 C17.7001081,11.7451987 18.3413669,11.1039398 18.3413669,10.312907 C18.3413669,9.52187408 17.7001081,8.88061523 16.9090752,8.88061523 L16.9090752,8.88061523 Z M4.56731796,4.58292633 L4.56731796,9.16625977 L2.84159347,9.16625977 C1.5759409,9.16625977 0.549926758,8.14024563 0.549926758,6.87459305 C0.549926758,5.60894047 1.5759409,4.58292633 2.84159347,4.58292633 L2.84159347,4.58292633 L4.56731796,4.58292633 Z M24.6584065,4.58292633 C25.9240591,4.58292633 26.9500732,5.60894047 26.9500732,6.87459305 C26.9500732,8.14024563 25.9240591,9.16625977 24.6584065,9.16625977 L22.932682,9.16625977 L22.932682,4.58292633 L24.6584065,4.58292633 Z');
@override
Widget build(BuildContext context) {
return Center(
child: Transform.translate(
offset: const Offset(-150, -150),
child: Transform.scale(
scale: 10,
child: CustomPaint(
painter: FilledPathPainter(
path: trianglePath,
color: Colors.blue,
),
),
),
),
);
}
}
class FilledPathPainter extends CustomPainter {
const FilledPathPainter({
required this.path,
required this.color,
});
final Path path;
final Color color;
@override
bool shouldRepaint(FilledPathPainter oldDelegate) =>
oldDelegate.path != path || oldDelegate.color != color;
@override
void paint(Canvas canvas, Size size) {
canvas.drawPath(
path,
Paint()
..color = color
..style = PaintingStyle.fill,
);
}
@override
bool hitTest(Offset position) => path.contains(position);
}
图片格式
图片常见格式出生年份表
| 格式 | ICO | GIF | Base64 | BMP | JPEG | PNG | SVG | JPEG2000 | APNG | WebP | HEIF |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 出生年份 | 1985 | 1987 | 1987 | 1990 | 1992 | 1996 | 1999 | 1997 -2000 | 2004 | 2010 | 2015 |
| 备注 | windows 1 | windows 3 | 链接 | 链接 | 链接 | 链接 | 链接 |
为什么有这么多图片格式?
我们知道,不同的图片格式有不同的压缩(编码)算法。因此,我认为图片格式的更新迭代,和计算机硬件算力,算法,带宽,版权共同决定的。
光栅图像的本质
我们常说的图像,实质上是由许许多多的像素点组成的二维矩阵。每一个像素点的数据结构如何? 值得我们研究。
1、二值图
二值图像(Binary Image),二值图像的表示只有两种形式,即0和1,其中0代表黑,1代表白。二值图像的保存相对简单,每个像素只需要1Bit就可以完整存储信息,因此二值图像中保存的信息较少。
2、灰度图
灰度图像(gray image)是每个像素只有一个采样颜色的图像,这类图像通常显示为从最暗黑色到最亮的白色的灰度。
灰度化就是使彩色图像的R、G、B分量相等的过程,即令R=G=B,此时的彩色表示的就是灰度颜色。
图片灰度化非常有用,我们常见的图像识别功能,第一步基本上图片灰度化,图像灰度化的目的是为了简化矩阵,提高运算速度,并且RGB三色对图片识别没有什么作用。
图像灰度化处理的几种方式
-
加权平均法
根据重要性及其它指标,将三个分量以不同的权值进行加权平均。由于人眼对绿色的敏感最高,对蓝色敏感最低,因此,按下式对RGB三分量进行加权平均能得到较合理的灰度图像。
-
平均值法
将彩色图像中的三分量亮度求平均得到一个灰度值。
-
最大值法
图片灰度化实践
通过以上的公式,可以很容易实现图片灰度化。对于图像识别,有时候灰度化之后,数据量依然很多的话,那还可以把灰度图片转为二值图。实现也简单。大于250/2,存1,小于250/2, 存0。二值图可用于快速计算识别图像轮廓。
| 原图 | 加权平均法 | 最大值法 | 平均值法 |
|---|---|---|---|
很明显,加权平均法计算出来的灰度图,人眼敏感度最高!
附源码
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path_drawing/path_drawing.dart';
import 'package:image/image.dart' as img;
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('灰度图片'),
),
body: const GrayImageTest(),
));
}
}
class GrayImageTest extends StatefulWidget {
const GrayImageTest({Key? key}) : super(key: key);
@override
State<GrayImageTest> createState() => _GrayImageTestState();
}
class _GrayImageTestState extends State<GrayImageTest> {
img.Image? image;
Future<img.Image?> loadImageFromAssets(String path) async {
ByteData data = await rootBundle.load(path);
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
return img.decodeImage(bytes);
}
@override
void initState() {
super.initState();
initData();
}
void initData() async {
image = await loadImageFromAssets('assets/image/Lenna.jpg');
final p = image!.getBytes();
for (var i = 0, len = p.length; i < len; i += 4) {
//加权平均法
final l = (0.299 * p[i] + 0.587 * p[i + 1] + 0.114 * p[i + 2]).round();
//最大值
//final l = max(max(p[i], p[i + 1]), p[i + 2]);
// 平均值
//final l = ((p[i] + p[i + 1] + p[i + 2]) / 3).round();
//分量法1
//final l = p[i];
p[i] = l;
p[i + 1] = l;
p[i + 2] = l;
}
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
return Center(
child: image == null
? const CircularProgressIndicator()
: Image(
image: MemoryImage(Uint8List.fromList(img.encodeJpg(image!))),
),
);
}
}
3、彩色图
彩色图像是指每个像素由R、G、B分量构成的图像。其中R、G、B由不同的灰度级来描述,每个分量的权重都介于[0,255],3字节(24位)可表示一个像素。对于包含透明分类构成的图片,那就是4字节(32位)
4、深度图(Depth)
深度图像(depth image)也被称为距离影像(range image),是指将从图像采集器到场景中各点的距离(深度)作为像素值的图像,它直接反映了景物可见表面的几何形状。
5、RGB-D图像
我们可以认为RGB-D是由两幅图像合在一起构成的,一个是普通的RGB三通道彩色图像,另一个是深度(Depth)图像。需要注意的是RGB-D图像中的RGB图像和Depth图像是严格配准的,即像素之间具有一对一的对应关系。
彩色图片二进制格式解析
图像原始数据
不管是什么格式,或采用什么样的压缩标准,原始的图像数据其实都是一样的。
例如,一张 4 × 4 (宽度和高度都是 4 个像素)的彩色图片,未压缩的的原始图像数据,就是一个 4 × 4 矩形网格,每一个网格代表一个像素。
从上图看出来,图片上的每一个像素,又是由 红,绿,蓝 三基色构成。上图右边所示,红绿蓝,对应于 r g b 三个数值,也就是我常说的 RGB 色彩模式。又称为颜色通道,彩色图像有三个通道值,每个颜色通道,都是一个 0~255 的整数值,占用一个字节(Byte)的存储空间。
因此,我们很容易计算上面这张 4×4 彩色图片占用的存储空间为 4 × 4 × 3 = 48 字节 (Bytes) 。换算成我们熟悉的 KB,就是 48 / 1024 = 0.046875 KB,不到 0.1 KB。
事实上,我们很少见到这么小的图片,甚至在我们的个人电脑和手机上,根本无法正常看到这么小的图片。
按照在电脑上常用的分辨率 72 ppi (Pixels Per Inch:像素每英寸),即 每 2.54 厘米 容纳 72 个像素,或者说,一个像素占用的屏幕尺寸是 0.35 毫米,那么上面 4 × 4 图片,在屏幕上 1:1 显示,占用屏幕的物理尺寸只有 1.4 × 1.4 毫米。显然,用肉眼是无法看清的。
如果以上对于图片大小的计算大家没什么感觉的话,下面我们在继续多一些对比
| 图片宽高像素 | 4*4 | 1024*1024 | 3024 * 4032 |
|---|---|---|---|
| 24位占用储存空间 | 0.05KB | 3M | 34.9M |
| 32位占用储存空间 | 0.06KB | 4M | 46.5M |
| 备注 | 一般图标 | 手机拍照像素 |
发现没,对于手机拍摄,图片原始数据占用的储存空间是不是有点过大。
如果大家还没啥感觉,我们不妨再用电影举例,一部宽高为 720 × 480,帧率为 30 帧/秒,时长为 1 小时的电影,其原始数据占用的大小大概是:
720 × 480 × 3 (字节/像素) × 30 (帧/秒) × 3600 (秒/小时) × 1小时 = 104.2 GB
是的,以上是图像采集设备采集到的原始数据需要储存空间大小!前文我们已经知道图片有很多种储存格式,其实每一种图片格式代表着不同的压缩算法。图像的压缩或编码,本质就是为了解决图像在存储和网络传输过程的空间消耗,让有限的磁盘和网络带宽,存储和传送海量的数字图像和视频提供了技术后盾。
下面,我们来看看不同图片格式的二进制编码
工具
在查看图片二进制数据之前,先介绍使用的工具
1. 在 linux 和 MacOS 系统上,我们可以借助一个 hexdump 来查看任何二进制文件
hexdump dog.jpeg
hexdump -e '16/1 "%02X "' -e '"\n"' dog.jpeg
hexdump -e '16/1 "%02X " " | "' -e '16/1 "%_p" "\n"' dog.jpeg
2. Hex Fiend
推荐1:下载链接:hexfiend.com/
3. Vs code
推荐2:安装 Hex Editor 插件
ICO
Windows 1.0时代的桌面图标文件格式,现在主要用户网站图标,一些桌面应用的图标。
00000100 0100``20``20 0000``0100`` 2000A810 00001600 0000
| 偏移量 | 字节数 | 示例中的值 | 含义 |
|---|---|---|---|
| 0 | 2 | 0000 | 保留字,都为0 |
| 2 | 2 | 0100 | 文件类型,1表示图标,2表示光标 |
| 4 | 2 | 0100 | 图像数量,一个icon文件可以有多个图标,这个为1 |
| 6 | 1 | 20 | 图像宽度,这个是0x20=32像素,1字节,说明ico的最大宽度是256像素 |
| 7 | 1 | 20 | 图像高度,这个是0x20=32像素,1字节,说明ico的最大高度是256像素 |
| 8 | 1 | 00 | 指定调色板中的颜色数。如果图像不使用调色板,则应为 0。 |
| 9 | 1 | 00 | 保留字,为0 |
| 10 | 2 | 0100 | 在 ICO 格式中:指定颜色平面。应该是 0 或 1。在 CUR 格式中:指定热点的水平坐标,以左侧的像素数为单位。 |
| 12 | 2 | 2000 | 在 ICO 格式中:指定每像素的位数。0x20=32位在 CUR 格式中:指定热点的垂直坐标,以距顶部的像素数为单位。 |
| 14 | 4 | A810 0000 | 指定图像数据的大小(以字节为单位) |
| 18 | 4 | 1600 0000 | 图标文件起始数据,一般都只1600 0000 |
目录之后是图标的数据。每个图标的数据可以是没有文件头的BMP图像,也可以是完整的PNG图像(包括文件头)。
GIF
GIF是一种使用LZW压缩,支持多张图像的容器。支持256色,透明通道为1bit。作为互联网表情包的载体,GIF这项80年代的技术依然生生不息。但它的弊端也是显而易见的:易出现毛边,色彩表现低劣,文件压缩比不高。
索引颜色
索引颜色是位图图片的一种编码方法。可以通过限制图片中的颜色总数的方法实现有损压缩。挑选一副图片中最有代表性的若干种颜色(通常不超过256种),编制成颜色表(Color Table)。在存储图片中每一个像素点的颜色信息时,不直接使用这个点的颜色值,而使用颜色表中的索引(Index)。
这样,要表示一幅32位真彩色的图片,使用索引颜色的图片只需要用不超过8位的颜色索引就可以表达同样的信息。使用索引颜色的位图常见的格式有GIF、PNG等。
上面案例gif的图像数据应该存储这些索引值
1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0......
GIF格式二进制图
Header 区块
示例图片
474946``38 3961
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 3 | 474946 | gif标志,ASCII码。它应该一直是”GIF” (ie 47=”G”, 49=”I”, 46=”F”) |
| 3 | 38 3961 | gif版本,示例中是89a, 还有一个97a |
Logical Screen Descriptor 区块(GIF基本信息)
0A00``0A00`` 910000
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 2 | 0A00 | gif的宽度,gif宽高使用小端编码,即实际宽度为0x000A=10, |
| 2 | 0A00 | gif的高度, 2字节,说明gif的最大宽高度是2^16=65536 |
| 1 | 91 | 是一个packed字节,字节91可以表示为二进制数10010001 |
| 1 | 00 | 背景颜色索引值 |
| 1 | 00 | 像素纵横比 |
GIF每帧数据
This content is only supported in a Feishu Docs
Graphics Control Extension
图形控制扩展块经常用于指定透明度设置和控制动画
21F90405 0A002000
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 2 | 21F9 | 固定,标志信息 |
| 1 | 04 | Byte Size,它的值总是4 |
| 1 | 05 | 是packed field。转为2进制如下 |
| 2 | 0A00 | 延迟时间值,实际时间为(0x000A=10)乘以0.01s=0.1s |
| 1 | 20 | 透明颜色索引,GIF没有半透明,如果一个图像的颜色索引是255,颜色表中索引255的颜色值是rgb(255,255,255),那么Image Data中存储255索引的像素最终在图像上的值就是rgba(255,255,255,0) |
| 1 | 00 | 最后一个字节是block终结者,它的值总是00 |
Image Descriptor
数据流中的每个图像都由一个图像描述符,可选的局部颜色表和图像数据组成。图像描述符可能跟在一个或多个控制区块(如Graphic Control Extension)后面
2C``0100``31 00``8E00``69 0000
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 1 | 2C | 起始标志,每一个图像帧都以一个图像描述符开始。这个block占10个字节。 |
| 2 | 0100 | Image Left,图像不一定占据logical screen descriptor定义的整个画布。因此就有left和top属性 |
| 2 | 31 00 | Image Top |
| 2 | 8E00 | 帧图宽度 |
| 2 | 69 00 | 帧图高度 |
| 1 | 00 | Packed Field |
Image Data
图像数据块使用LZW压缩算法,对图片每一帧进行压缩。
Application Extension
第三方应用拓展该规范允许将应用程序特定信息嵌入到GIF文件本身中。都是已0x21FF标志位开头。
重点说一下里面有一个gif播放循坏次数。下图图的值是0,它代表永久循环。
Comment Extension
注释块, 可以在GIF文件中嵌入注释。也许这将是一个有趣的方式来传递秘密信息。这种扩展标签值为0x21FE。
Trailer结尾标志
3B
BMP
位图(Bitmap)格式其实并不能说是一种很常见的格式,因为其数据没有经过压缩,或最多只采用行程长度编码(RLE,run-length encoding)来进行轻度的无损数据压缩。正是因为它没有进行数据压缩,其内部存储的色彩信息(灰度图,RGB 或 ARGB)直接以二进制的形式暴露在外,也十分方便借助计算机软件进行简单或深入的分析。
位图文件头 Bitmap File Header
我们日常生活中遇到的 .bmp 格式图片的文件头长度绝大多数都是 54 字节,其中包括 14 字节的 Bitmap 文件头(Bitmap File Header)以及 40 字节的 DIB (bitmap information header) 数据头。
示例图片
424D``0E0F 0000``0000 00003600 0000 14bytes
| 索引 | 字节数 | 值 | 值含义 |
|---|---|---|---|
| ① | 2 | 424D | 标记代码 |
| ② | 4 | 0E0F 0000 | 整个.bmp文件的大小(little endian)0x0F0E=3854字节 |
| ③ | 4 | 0000 0000 | 预留字段,通常为0 |
| ④ | 4 | 3600 0000 | 图片信息的开始位置 |
位图信息文件头DIB header (bitmap information header)
28000000 ``21000000`` DAFFFFFF ``0100`` 1800 00000000 B20E0000 ``130B0000`` 130B0000 00000000 00000000 40字节
| 示例值 | 偏移位10进制 | 字节数 | 含义 |
|---|---|---|---|
28000000 | 14 | 4 | DIB header的大小,通常都是0x28=40字节 |
21000000 | 18 | 4 | 以像素为单位的位图宽度(有符号整数)0x21=33 |
DAFFFFFF | 22 | 4 | 以像素为单位的位图高度(有符号整数)为-38,而当高度为负值时,数据块中的行记录与实际图像才是同序的,否则为反序。 |
0100 | 26 | 2 | 色彩平面(color plane)的数量(必须为 1) |
1800 | 28 | 2 | 每个像素的位数,即图像的颜色深度。典型值为 1、4、8、16、24 和 32。 |
00000000 | 30 | 4 | 正在使用的压缩方法。通常不压缩,对应值为0 |
B20E0000 | 34 | 4 | 图像大小。这是原始位图数据的大小;0x0EB2=3762 |
130B0000 | 38 | 4 | 图像的水平分辨率。(每米像素,有符号整数) |
130B0000 | 42 | 4 | 图像的垂直分辨率。(每米像素,有符号整数) |
00000000 | 46 | 4 | 调色板中的颜色数,通常为0 |
00000000 | 50 | 4 | 使用的重要颜色的数量(没啥用),通常为 0,表示每种颜色都重要 |
| 共40 |
原始位图数据 Raw Bitmap Data
由于BMP基本上都是没有压缩的,对于RGB为[蓝,绿, 红]排列;对于aRGB图片为[蓝,绿, 红, a],通过读取这个数据,就可以还原一张bmp格式图片了。在解码的时候,每一行需要seek多少个字节呢?通过下面的公式计算就行:
每行的字节数等于:每像素比特数乘以图片宽度加 31 的和除以 32,并向下取整,最后乘以 4。
这样,就很容易计算出原始位图数据的大小为:
PixelArraySize = RowSize · | 图像高度 |
举一个例子:
这样,我们就可以计算出示例图片的RowSize = ( 24 * 33 + 31 ) / 32 * 4 = 100
总数据大小 = 100 * 38 = 3800 + header(54) = 3854。哇!数据刚好对上。
在解析的时候每行 100 / 3 = 33 ... 1,故每行最后会补一个字节的0x00。
JPG
jpg基本上是图片的代名词了。平常我们大部分见到的静态图基本都是这种图片格式。图片能比较好的表现各种色彩,主要在压缩的时候会有所失真,也正因为如此,造就了这种图片格式体积的轻量。
优点: 压缩率高;兼容性好;色彩丰富
缺点: JPEG不适合用来存储企业Logo、线框类的这种高清图;不支持动画、背景透明
1.格式简介
每段数据格式:
| FF开头 | 标志位 | 数据长度 | 数据 |
|---|
| SOI | ff d8 | 文件开始 |
|---|---|---|
| APP0 | ff e0 | 定义交换格式和图像识别信息 |
| DQT | ff db | 定义量化表 |
| SOF0 | ff c0 | 帧开始 |
| DHT | ff c4 | 霍夫曼(Huffman)表 |
| SOS | ff da | 扫描行开始 |
| EOI | ff d9 | 文件结束 |
2. 文件头标识 (2 bytes) (SOI (Start Of Image))
标志位: FFD8
3.文件结束标识 (2 bytes) (EOI)
标志位: FFD9
4. APP0-应用程序保留标记0
标志位:FFE0
FFE00010 ``4A464946 00``0101`` 00 ``0048``0048 0000
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 2 | 0010 | 指该块长度为16个字节(不包括FFEO) |
| 5 | 4A464946 00 | 是JFIF格式标识码,转为ASCII 就是JFIF |
| 2 | 0101 | 是版本号(第一个01是主版本号,第二个01是次版本号) |
| 1 | 00 | X和Y的密度单位 ****00是单位(00=无单位;01=点数/英寸;02=点数/厘米) |
| 2 | 0048 | X方向像素密度 **72 |
| 2 | 0048 | Y方向像素密度** |
| 1 | 00 | 缩略图水平像素数目,(没有微缩图像,一般都没有,值为0) |
| 1 | 00 | 缩略图垂直像素数目,(没有微缩图像,值为0) |
| 3* N | 无 | 缩略图RGB位图数据 |
5. **APPn, Application,应用程序保留标记n,其中n=1~15(任选)
标志位:0xFFE1~0xFFEF
| 索引 | 字节数 | 值 | 值含义 |
|---|---|---|---|
| ① | 2 | 0xFFE1~0xFFEF | 标记代码 |
| ② | 2 | ②,③字段的总长度,即不包括标记代码,但包括本字段 | |
| ③ | 内容不定 | 详细信息 |
Adobe Photoshop 生成的JPEG图像中就用了APP1和APP13两个标记段分别存储了一幅图像的副本。
6. SOF0-图像基本信息,帧图像开始(Start of Frame)
标志位: FFCO
FFC00011 08``0780``04 38``03``0122 00021101 031101
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 2 | 0011 | SOF0块长度为17个字节 |
| 1 | 08 | 每个像素的每个颜色分量为8位 |
| 2 | 0780 | 图片高度为1920(直接换算成十进制)2个字节,故jpg最大高度2^16=65536 |
| 2 | 04 38 | 图片宽度1080(直接换算成十进制) |
| 1 | 03 | 颜色分量数3,JPEG一般采用yCrCb格式,因此最后的组件数量通常为3(每个组件就是像素的一个颜色分量)y指的是亮度;Cr指的是红色分量;Cb指的是蓝色分量。 |
| 9 | 012200``021101``031101 | 颜色分量信息(重复出现,有多少个颜色分量,就出现多少次(一般为3次))01颜色分量ID 1字节22水平/垂直采样因子 1字节00量化表 1字节 当前分量使用的量化表的ID |
7. jpg色彩空间
jpg的色彩空间使用YCbCr
| RGB | CMYK | YCbCr |
|---|---|---|
| Y表示的是亮度,Cb表示的是彩度(蓝),Cr表示的是彩度(红) | ||
| 通常RGB的色彩模型用于显示屏的显示 | 青色、品红、黄色和黑色通常CMYK的色彩模型用于印刷 | jpg压缩 |
RGB和YCbCr转换公式
jpg为什么使用YCbCr作为色彩空间?
我们的眼睛之所以能感知图像,是因为人眼内含有视锥细胞和视杆细胞,其中,视锥细胞具有感知颜色的能力,而视杆细胞具有感知亮度的能力,通常,我们的眼睛中,视杆细胞数量相对较多,所以人眼对亮度的敏感程度要高于对色彩的敏感程度。就像你熄灯时,你可以在暗光下渐渐地看清周围的事物,而对周围事物的颜色,你可能就不那么敏感了。
JPEG正是利用了人眼的这一特性,在压缩图像时,将亮度和颜色分开处理。在YCbCr模型中,Cb通道和Cr通道中所包含的信息量远远少于Y通道中包含的信息量。JPEG的压缩算法主要对Cb和Cr通道中的数据进行缩减取样,取样的比例可以是4:4:4(无缩减取样)、4:2:2(在水平方向2的倍数中取样)和4:2:0(在水平方向和垂直方向的2的倍数中取样),其中,以4:2:0最为常见。
8. 应用场景一
我们可能有需求,获取云端图片的宽高,一般的做法是把图片下载下来,然后对图片进行解码,解码之后再获取图片的宽高信息。
有了以上的知识,我们可以使用HTTP请求头Header,添加range=bytes=1-1600,这样就并不需要把图片完整的下载,就可以获取图片的宽高信息。假如我们验证图片宽高符合我们的预期,依然可以设置range=bytes=1601-,下载剩余部分数据,下载完成之后两份数据拼接保存就行。
附代码
final _firstChunkSize = 1600;
final url =
"http://fanbook-1251001060.cos.accelerate.myqcloud.com/fanbook/app/files/chatroom/image/0a34c1abd07cd2a053b6be03022010b8.jpg";
void getRemoteFile() async {
final rep = await _dio.get(
url,
options: Options(
headers: {"range": "bytes=0-$_firstChunkSize"},
responseType: ResponseType.stream,
),
);
if (rep.statusCode == 206) {
final stream = await (rep.data as ResponseBody).stream.toList();
final result = BytesBuilder();
for (Uint8List subList in stream) {
result.add(subList);
}
final imgData = result.takeBytes();
for (int i = 0; i < imgData.length - 8; i++) {
if (imgData[i] == 0xff && imgData[i + 1] == 0xc0) {
final h = (imgData[i + 5] << 8) + imgData[i + 6];
final w = (imgData[i + 7] << 8) + imgData[i + 8];
debugPrint("图片高度:$h 宽度: $w");
break;
}
}
}
}
PNG
PNG是一种无损压缩的位图片形格式,其设计目的是试图替代GIF和JPG文件格式,同时增加一些GIF文件格式所不具备的特性。
PNG格式有8位、24位、32位三种形式,其中8位PNG支持两种不同的透明形式(索引透明和alpha透明),24位PNG不支持透明,32位PNG在24位基础上增加了8位透明通道,因此可展现256级透明程度。
PNG的优势
对于PNG这种图像存储格式,它有两个特点:无损压缩和支持透明效果。
由于PNG文件采用LZ77算法的派生算法进行压缩,其结果是获得高的压缩比,不损失数据。它利用特殊的编码方法标记重复出现的数据,因而对图像的颜色没有影响,也不可能产生颜色的损失,这样就可以重复保存而不降低图像质量。
PNG可以为原图像定义256个透明层次,使得彩色图像的边缘能与任何背景平滑地融合,从而彻底地消除锯齿边缘。这种功能是GIF和JPEG没有的。
PNG数据块(Chunk)
PNG定义了两种类型的数据块,
- 关键数据块(critical chunk),这是标准的数据块。下表标红的部分。
- 辅助数据块(ancillary chunks),这是可选的数据块。
| 数据块符号 | 数据块名称 | 多数据块 | 可选否 | 位置限制 |
|---|---|---|---|---|
| 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 | 图像结束数据 | 否 | 否 | 最后一个数据块 |
数据块中有 4 个关键数据块:
- 文件头数据块 IHDR(header chunk):包含有图像基本信息,作为第一个数据块出现并只出现一次。
- 调色板数据块 PLTE(palette chunk):必须放在图像数据块之前。
- 图像数据块 IDAT(image data chunk):存储实际图像数据。PNG 数据允许包含多个连续的图像数据块。
- 图像结束数据 IEND(image trailer chunk):放在文件尾部,表示 PNG 数据流结束。
数据块连起来,大概这个样子:
| PNG 标识符 | PNG 数据块(IHDR) | PNG 数据块(其他类型数据块) | ... | PNG 结尾数据块(IEND) |
|---|
PNG数据块结构
PNG文件中,每个数据块由4个部分组成,如下:
| 名称 | 字节数 | 说明 |
|---|---|---|
| Length (长度) | 4字节 | 指定数据块中数据域的长度,其长度不超过(231-1)字节。该长度只包含数据块的长度。 |
| Chunk Type Code (数据块类型码) | 4字节 | 数据块类型码由ASCII字母(A-Z和a-z)组成 |
| Chunk Data (数据块数据) | 可变长度 | 存储按照Chunk Type Code指定的数据 |
| CRC (循环冗余检测) | 4字节 | 存储用来检测是否有错误的循环冗余码 |
PNG标识符
举例图片如下
89504E47 0D0A1A0A 共8个字节,png格式固定标识头。其中第一个字节0x89超出了ASCII字符的范围,这是为了避免某些软件将PNG文件当做文本文件来处理。
文件头数据块IHDR
它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。
文件头数据块由13字节组成,它的格式如下表所示。
整块含义解析
| 0000000D | 49484452 | 000003F1 000003CB 08030000 00 | 20FEF9 6C |
|---|---|---|---|
| 数据块长度 0x0000000D = 13个字节 | IHDR标志的ASCII码 | 数据部分 | CRC 校验位 |
| 4字节 | 4字节 | 13字节 | 4字节 |
数据块含义解析
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 4 | 000003F1 | Width。图像宽度,以像素为单位 0x3F1=1009。占4字节,PNG的最大宽高度是2^31 - 1=2,147,483,647。实际上,如果一个图片过大,会由于内存耗尽而无法读取。 |
| 4 | 000003CB | Height。图像高度,以像素为单位 0x3CB=971 |
| 1 | 08 | 图像深度(Bit depth): 即每一个分量的颜色占的位数,示例为2^8=256,红(0-256) |
| 1 | 03 | 颜色类型(ColorType): 0:灰度图像, 允许的位深度:1,2,4,8或16 2:真彩色图像,允许的位深度:8或16 3:索引彩色图像,允许的位深度:1,2,4或8 4:带α通道数据的灰度图像,允许的位深度:8或16 6:带α通道数据的真彩色图像,允许的位深度:8或16 |
| 1 | 00 | Comdivssion method 压缩方法(LZ77派生算法)。一般都是0 |
| 1 | 00 | Filter method滤波器方法 |
| 1 | 00 | Interlace method隔行扫描方法: 0:非隔行扫描 1: Adam7(由Adam M. Costello开发的7遍隔行扫描方法) |
调色板数据块PLTE
调色板数据块PLTE(palette chunk)包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。
PLTE数据块是定义图像的调色板信息,PLTE可以包含1~256个调色板信息,每一个调色板信息由3个字节组成.因此,调色板的长度应该是3的倍数,否则,这将是一个非法的调色板
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 4 | 00000300 | 数据块长度 00000300=768 字节 |
| 4 | 504C5445 | 数据块类型码 “PLTE” 的 ASCII 字母 |
| 768 | 4C697100 A0E9E400 7FE4007F .....04382 | 768/3=256, 说明索引颜色为256个,这个和前面分析的,颜色类型0x03,即这个索引表不包括透明通道。每一个颜色占3个字节,示意如下:4C6971 R=4C G=69 B=71 |
| 4 | E52BA8F5 | CRC (循环冗余检测) |
这一堆索引颜色16进制值,我们没有直观感受,写了一个Demo,把索引颜色表达出来。效果如下
附源码
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:path_drawing/path_drawing.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('PNG 索引彩色'),
),
body: const PngIndexColorTestPage(),
));
}
}
class PngIndexColorTestPage extends StatefulWidget {
const PngIndexColorTestPage({Key? key}) : super(key: key);
@override
State<PngIndexColorTestPage> createState() => _PngIndexColorTestPageState();
}
class _PngIndexColorTestPageState extends State<PngIndexColorTestPage> {
List<int> indexColorList = [];
@override
void initState() {
super.initState();
loadColorData();
}
void loadColorData() {
String indexStr = '''
4C697100 A0E9E400 7FE4007F FFF10000 A0E9FFF1 00E4007F E4007F00 A0E900A0 E900A0E9 FFF100E4 007FFFF1 00FFF100 00A0E9FF F100E400 7FFFF100 E4007F00 A0E9E400 7F00A0E9 FFF100E4 007F00A0 E9E4007F E4007F00 A0E9FFF1 00FFF100 00A0E90D 0C0CFFF1 0000A0E9 009F40E6 001E00A0 E900A0E9 E60C10E5 004DE400 7F800F85 FFF100FF F1000099 48E4007F E4007FE4 007F4245 40009A51 009D8AE8 3B0B009B 4300A0E9 E5005B18 248A009F A7E60015 E6003B20 1F1FFFF1 00E6002C 013A95FF F1005448 7F423834 009A61AB 5B3F875D 4B033392 004FA355 775E3534 32595073 009C7324 1F88ED70 0046B232 0A090946 1C870069 B850815F 1AA93A51 6255F7AD 00E60026 0077C30F 0E0EEF7F 00009D95 07A33D55 4E64EB61 00F9BB00 009C82AC CE049306 83493D38 614E44A6 01826F55 49005DAE 55705CE7 220E0F2A 8D2E1E88 584D7AF2 9100E601 4845895D 53443E99 C715009F B5383438 4540496A B82C915E 4900459C 4B544B00 9A5A4B1B 87BDD400 E94F0684 C0235A1A 863D1D87 9F5E4625 2322534D 60242322 6C168674 574A77BC 28681786 3C373BF4 A1004B46 524E4958 E94609F1 8A00009B 73413C43 B65738E7 1E0F84C1 29585070 F49E0063 B62F2D8E 5A494085 777759E6 00120000 00E4007F 00A0E9FF F1000099 441D2088 FFFFFFF9 D4E3F19E C2EA609E FFF352FF F20CFFE7 0001B4ED FCEBF2F3 ACCBE738 8EF5EB00 00974CFB DFEBDD20 19079355 E94C96F5 F6F62321 217FCEF4 E5006800 A5EAE500 73C8E8FA 009AE3FF FDEAED80 B000A0C1 EF8FB92B 2887D63A 2000AFEC A7DBF7F3 FAFEE400 7A0085D0 00A0DF00 A9EBE61C 87FFFDD4 FFF89BFE F5F9FFFE F5FFFAB7 E6F4FD49 3D84F5BA D3F7C7DB FCC800FF F5785653 53008DD7 D7EEFCFF DD00EC70 A7413786 EBECEC00 A0CBCFDB 00DFE0E0 45424166 64649FA0 A0BE0081 00A0D648 C0F093D5 F6EBE600 D2008024 BAEFD4D4 D5DDE000 C34F2FCC 46283630 8763C7F2 C8008100 94DEB7E2 F9838383 B3008291 9292ADAE AEBABBBB C7C8C8FD D200DB00 7F757474 288E5AFF F689FFFB C4FFF465 FFF332B8 5637C0D5 00504382
'''
.replaceAll(" ", "");
List<int> index = [];
for (int i = 0; i < indexStr.length - 2; i += 2) {
final item = indexStr.substring(i, i + 2);
int colorItem = int.parse("0x$item");
debugPrint("colorItem: $colorItem");
index.add(colorItem);
}
indexColorList.clear();
indexColorList.addAll(index);
debugPrint("indexColorList length: ${indexColorList.length}");
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
if (indexColorList.isEmpty) return Container();
//768 / 3 = 256 索引颜色一个256个
List<Widget> children = [];
for (int i = 0; i < 16; i++) {
List<Widget> columnChildren = [];
for (int j = 0; j < 16; j++) {
final startIndex = i * 16 + j;
columnChildren.add(
Expanded(
child: buildZone(indexColorList[startIndex],
indexColorList[startIndex + 1], indexColorList[startIndex + 2]),
),
);
}
children.add(Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: columnChildren,
)));
}
return Center(
child: Padding(
padding: const EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
const Text("原图"),
Image.asset(
"assets/image/RGB-02.png",
width: 80,
height: 80,
),
],
),
const SizedBox(width: 10),
SizedBox(
width: 800,
height: 800,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
),
],
),
));
}
Widget buildZone(int r, int g, int b) {
final color = Color.fromARGB(
0xFF,
r,
g,
b,
);
return Stack(
children: [
Positioned.fill(
child: ColoredBox(color: color),
),
Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"r:$r",
style: const TextStyle(fontSize: 9, color: Colors.white),
),
Text(
"g:$g",
style: const TextStyle(fontSize: 9, color: Colors.white),
),
Text(
"b:$b",
style: const TextStyle(fontSize: 9, color: Colors.white),
),
],
))
],
);
}
}
图片压缩数据IDAT
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 4 | 00002000 | 数据块长度 00002000=8192 字节 |
| 4 | 49444154 | 数据块类型码 “IDAT” 的 ASCII 字母 |
| 8192 | ... | 图片数据 |
| 4 | CRC (循环冗余检测) |
IDAT块包含实际的图像数据,它是压缩算法的输出流。详见第 9 条:过滤和第 10 条:压缩。
图像结束标识IEND
00000000 49454E44 AE426082 共12字节。
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 4 | 00000000 | 数据库长度=0 |
| 4 | 49454E44 | 数据标识总是IEND(49 45 4E 44) |
| 0 | 数据库 | |
| 4 | AE426082 | CRC |
当IEND数据块被找到时,这个PNG图像才认为是合法的PNG图像。
WebP
是一种新的图片格式,可提供出色的无损和有损压缩,对于Web开发来说,可以创建更小和更丰富的图像。根据官网测试,WebP无损压缩的图片比PNG格式图片,文件大小上少 26%,WebP有损图片在同样 SSIM 质量指标上比JPEG格式图片少25~34%,SSIM是一种衡量两张数字影像相似的指标。
压缩举例
| WebP和Png大小对比 | Gif和Animated WebP大小对比 | WebP和Jpg大小对比 |
|---|---|---|
| 在线示例 | 在线示例 | 在线示例在线示例2 |
科技博客 GigaOM 曾报道:YouTube 的视频略缩图采用 WebP 格式后,网页加载速度提升了 10%;谷歌的 Chrome 网上应用商店采用 WebP 格式图片后,每天可以节省几 TB 的带宽,页面平均加载时间大约减少 1/3;Google+ 移动应用采用 WebP 图片格式后,每天节省了 50TB 数据存储空间。
根据Google的测试,目前WebP与JPG相比较,编码速度慢10倍,解码速度慢1.5倍。
在编码方面,一般来说,我们可以在图片上传时生成一份WebP图片或者在第一次访问JPG图片时生成WebP图片,对用户体验的影响基本忽略不计,主要问题在于1.5倍的解码速度是否会影响用户体验。
下面通过同样质量(SSIM)的WebP与JPG图片加载的速度进行测试。
从折线图可以看到,WebP虽然会增加额外的解码时间,但由于减少了文件体积,缩短了加载的时间,页面的渲染速度加快了。同时,随着图片数量的增多,WebP页面加载的速度相对JPG页面增快了。
二进制格式解析
实例图片
储备知识
RIFF 资源互换文件格式(Resources Interchange File Format)
它是在1991年时,由 Microsoft 和 IBM提出。是一种把资料储存在被标记的区块(tagged chunks)中的档案格式(meta-format)。 RIFF使用的是 小端序(little-endian)。
'RIFF'类型的chunk结构如下
structchunk{
u32 id; /* 块标志 */
u32 size; /* 块大小 */
/*此时的dat = type + restdat */
u32 type ; /* 类型 */
u8 restdat[size] /* dat中除type4个字节后剩余的数据*/
};
WebP文件格式基于RIFF(资源交换文件格式)文档格式。
FourCC (four-character code):32 bits, 代表四字符编码字符串。比如WebP(0x``57454250)
Chunk Size: 32 bits , 数据块(Chunk Payload)大小,不包括自己和FourCC
Chunk Payload: ****数据块
WebP Chunk类型
| ChunkType | 描述 |
|---|---|
| VP8X | descriptions of features used。包含图片的宽高,是否透明,是否动画等信息 |
| ALPH | alpha bitstream,透明通道数据流 |
| VP8 | bitstream,表示有损压缩数据流 |
| VP8L | lossless bitstream 表示无损压缩数据流 |
| ICCP | color profile 颜色配置 |
| XMP | metadata |
| ANIM | global animation parameters 全局动画信息,比如动画循环次数 |
| ANMF | frame1 parameters + data, 每一帧动画参数和数据 |
| EXIF | metadata,可交换图像文件,保存相机参数,位置,版权等信息 |
带有alpha通道有损编码图像可能如下所示
RIFF/WEBP
+- VP8X (descriptions of features used)
+- ALPH (alpha bitstream)
+- VP8 (bitstream)
无损编码图像可能如下所示:
RIFF/WEBP
+- VP8X (descriptions of features used)
+- VP8L (lossless bitstream)
具有ICC配置文件和XMP元数据的无损图像可能如下所示:
RIFF/WEBP
+- VP8X (descriptions of features used)
+- ICCP (color profile)
+- VP8L (lossless bitstream)
+- XMP (metadata)
带有Exif元数据的动画图像可能如下所示:
RIFF/WEBP
+- VP8X (descriptions of features used)
+- ANIM (global animation parameters)
+- ANMF (frame1 parameters + data)
+- ANMF (frame2 parameters + data)
+- ANMF (frame3 parameters + data)
+- ANMF (frame4 parameters + data)
+- EXIF (metadata)
WebP标志头
52494646`` ``94120000`` ``57454250
| 字节数 | 示例中的值 | 示例中的含义 |
|---|---|---|
| 4 | 52494646 | 代表RIFF的ASCII码,RIFF是一个存数据的容器 |
| 4 | 94120000 | wepb使用小端编码, 这部分代码总数据大小 0x1294=4756 + 8 = 4764 |
| 4 | 57454250 | 代表WEBP的ASCII码 |
图片描述信息VP8X
56503858`` 0A000000 10000000 C70000C7 0000
| 字节数 | 示例中的值(16进制) | 示例中的含义 |
|---|---|---|
| 4 | 56503858 | 代表VP8X的ASCII码 |
| 4 | 0A000000 | 数据块长度 0x0A = 10 |
| 1 | 10 | 转为2进制为:00010000,一共8个bite。含义如下00: 保留位, 2bit0: Set if the file contains an ICC profile. ICC1: 是否为透明图片,alpha0:是否包含Exif信息(Exif metadata)0:是否包含XMP信息( XMP metadata)0: 是否为动画图片,为1说明Data in 'ANIM' and 'ANMF' chunks0: 保留位 |
| 3 | 000000 | 保留字段,都为0 |
| 3 | C70000 | ****Canvas Width **实际宽度为:1 + 0xC7 = 200。**WebP 与 VP8 比特流兼容,并使用 14 位来表示宽度和高度。WebP 图像的最大像素尺寸为 16383 x 16383 |
| 3 | C70000 | Canvas Height 实际高度度为:1 + 0xC7 = 200 |
ALPH 图片alpha通道数据
414C5048`` B7090000 ``01``F74736 6993E4FF D34B......
| 字节数 | 示例中的值(16进制) | 示例中的含义 |
|---|---|---|
| 4 | 414C5048 | 代表ALPH的ASCII码 |
| 4 | B7090000 | 数据块长度 0x9B7 = 2487(奇数的话,需要在数据库末尾补0,让长度变成偶数。) |
| 1 | 01 | 转为2进制为:00000001,一共8个bite。含义如下00: 保留位, 2bit00: 数据是否已经预处理,0没有预处理,100: 滤波方式01: 压缩方式,0:没有压缩,1: WebP无损压缩 |
| 2487 | ... | alpha通道数据流 |
ANIM图片动画信息
示例图片
414E494D`` ``06000000`` ``FFFFFF00`` ``0000
| 字节数 | 示例中的值(16进制) | 示例中的含义 |
|---|---|---|
| 4 | 414E494D | 代表ANIM的ASCII码 |
| 4 | 06000000 | 数据块长度 0x06 = 6字节 |
| 4 | FFFFFF00 | 画布的默认背景颜色,以 [Blue, Green, Red, Alpha] 字节顺序排列#``FFFFFF00,白色透明背景 |
| 2 | 0000 | 动画循环次数,0代表无限循环 |
ANMF每帧动画图像
414E4D46`` ``D6010000`` 780000 ``5A 0000``7502 00``650300`` ``640000``03 5650384C......
| 字节数 | 示例中的值(16进制) | 示例中的含义 |
|---|---|---|
| 4 | 414E4D46 | 代表ANMF的ASCII码 |
| 4 | D6010000 | 数据块长度 0x01D6 = 470字节 |
| 3 | 780000 | 帧 X, 实际左上角的 X 坐标为**Frame X * 2**Offx = 0x78 * 2 = 240 |
| 3 | 5A 0000 | 帧 Y, 实际左上角的 Y 坐标为Frame Y * 2Offy = 0x5A * 2 = 180 |
| 3 | 7502 00 | 帧宽减一实际宽度为1 + 0x0275 = 630 |
| 3 | 650300 | 帧高减一实际高度为1 + 0x0365 = 870 |
| 3 | 640000 | 帧持续时间,显示下一帧之前的等待时间,以 1 毫秒为单位0x64/1000=0.1s |
| 1 | 03 | 转为2进制为:0000001``1,一共8个bite。含义如下000000: 保留位,必现为01:混合方式(B), 指示当前帧的透明像素如何与前一个画布的相应像素混合:- 0:使用 Alpha 混合。处理前一帧后,使用 alpha 混合在画布上渲染当前帧。如果当前帧没有 alpha 通道,假设 alpha 值为 255,有效地替换了矩形。- 1: 不要混合。处理前一帧后,通过覆盖当前帧覆盖的矩形在画布上渲染当前帧。1:处理方法(D), 指示当前帧在画布上显示后(在渲染下一帧之前)如何处理:- 0: 不要丢弃。让画布保持原样。- 1:设置为背景颜色。用ANIM 块中指定的背景颜色 填充当前帧覆盖的画布上的矩形。 |
| 470-16 | ... | 帧数据 |
附参考文档:
cloud.tencent.com/developer/a…
developers.google.cn/speed/webp/…
www.media.mit.edu/pia/Researc…