A GIF 解码器:一个 Go 接口的编码示例
Rob Pike
2011 年 5 月 25 日
介绍
在 2011 年 5 月 10 日于旧金山举行的 Google I/O 大会上, 我们宣布 Go 语言现已在 Google App Engine 中提供. Go 是 App Engine 上第一种可直接编译为机器代码的语言, 这使其成为 CPU 密集型任务 (如图像处理) 的理想选择.
因此, 这里我们将展示一个叫做 Moustachio 的程序, 该程序可以轻松的改善这样的图像:
添加小胡子并共享结果:
所有图形处理, 包括渲染抗锯齿的胡子都由在 App Engine 上运行的 Go 程序完成. (可以从这里取得源码 appengine-go 项目.)
尽管网络上的大多数图像 (至少可能是经过处理的图像) 都是 JPEG, 但周围还有无数其他格式, 对于 Moustachio 来说, 接受其中的一些图像似乎是合理的. JPEG 和 PNG 解码器已经存在于 Go 图像库中, 但是没有显示古老的 GIF 格式, 因此我们决定在公告时及时编写一个 GIF 解码器. 该解码器包含一些片段, 这些片段演示了 Go 的界面如何使某些问题更易于解决. 本博客文章的其余部分描述了几个实例.
GIF 格式
首先, 快速过一遍 GIF 格式. 调色板 是 GIF 图像文件, 也就是说, 每个像素值都是文件中包含的固定颜色图的索引. GIF 格式可以追溯到显示屏上每个像素通常不超过8位, 并且使用彩色图将有限的一组值转换为在屏幕点亮 LED 所需的 RGB(红色, 绿色, 蓝色)三元组. (这与没有颜色映射的 JPEG 相反, 例如, 编码分别表示不同的颜色信号。)
一张 GIF 图片每个像素可以包含1到8位, 但比较常见的还是每个像素 8 位.
稍微简化一下, GIF 文件包含一个定义像素深度和图像尺寸的标头, 一个颜色图(对于 8 位图像, 为256 RGB 三元组), 然后是像素数据. 像素数据存储为一维位流, 使用 LZW 算法进行压缩, 这对计算机生成的图形非常有效, 尽管对摄影图像不太好. 然后, 将压缩后的数据分解为长度限制的块, 其长度为一字节 (0-255)后跟这么多字节:
解码像素数据
要在 Go 中解码 GIF 像素数据, 我们可以使用 compress/lzw
包中的 LZW 解压缩器. 它有一个 NewReader 函数, 该函数返回一个对象, 该对象如 文档所述, "通过解压缩从 r 读取的数据来满足读取要求":
func NewReader(r io.Reader, order Order, litWidth int) io.ReadCloser
此处的 order
定义为位包的顺序以及 litWidth
是字的位大小, 对于 GIF 文件其对应为像素深度, 通常为 8.
但是我们不能仅将 NewReader
输入文件作为其第一个参数, 因为解压缩器需要字节流, 但是 GIF 数据是必须解压缩的块流. 为了解决这个问题, 我们可以使用一些代码包装输入 io.Reader
来对其进行解块, 然后使该代码再次实现 Reader
. 换句话说, 我们将解码代码块放入了新类型的 Read
方法中, 我们将其称为 blockReader
.
这是 blockReader
的数据结构.
type blockReader struct {
r reader // 输入源; 实现 io.Reader 和 io.ByteReader.
slice []byte // 未读数据的缓冲区.
tmp [256]byte // 用于切片的存储.
}
读取器 r
将成为图像数据的来源, 可能是文件或 HTTP 连接. slice
和 tmp
字段将用于管理解块. 这是完整的 Read
方法. 这是在 Go 中使用切片和数组的一个很大的例子.
1 func (b *blockReader) Read(p []byte) (int, os.Error) {
2 if len(p) == 0 {
3 return 0, nil
4 }
5 if len(b.slice) == 0 {
6 blockLen, err := b.r.ReadByte()
7 if err != nil {
8 return 0, err
9 }
10 if blockLen == 0 {
11 return 0, os.EOF
12 }
13 b.slice = b.tmp[0:blockLen]
14 if _, err = io.ReadFull(b.r, b.slice); err != nil {
15 return 0, err
16 }
17 }
18 n := copy(p, b.slice)
19 b.slice = b.slice[n:]
20 return n, nil
21 }
第 2-4 只是一项完整性检查: 如果没有放置数据的地方, 则返回零. 那将永远不会发生, 但是安全是一件好事.
第 5 行通过检查 b.slice
的长度来询问上一次调用是否剩余数据. 如果没有, 则切片的长度为零, 我们需要从 r
中读取下一个块.
GIF 块以字节计数开始, 在第6行读取. 如果计数为零, 则 GIF 将其定义为终止块, 因此我们在第 11 行返回 EOF
.
接下来我们知道应该读取 blockLen
字节, 因此将 b.slice
指向 b.tmp
的第一个 blockLen
字节, 然后使用辅助函数 io.ReadFull
读取那么多字节. 如果该函数无法读取那么多字节, 则该函数将返回错误, 这永远不会发生. 因为我们已经准备好读取 blockLen
个字节.
第 18-19 行将数据从 b.slice
复制到调用者的缓冲区. 我们正在实现 Read
, 而不是 ReadFull
, 因此允许返回的字节数少于请求的字节数. 这很容易: 我们只需将数据从 b.slice
复制到调用者的缓冲区 (p
), 复制的返回值就是传输的字节数. 然后我们对 b.slice
进行切片以删除前一个 n
字节, 为下一次调用做好准备.
在 Go 编程中将切片 (b.slice
)与数组(b.tmp
)耦合是一种很好的技巧. 在这种情况下,这意味着 blockReader
类型的 Read
方法从不进行任何分配. 这也意味着我们不需要保持计数 (它在切片长度中是隐含的), 并且内置的 copy
函数保证我们永远不会复制过多的副本. (有关切片的更多信息可参阅 来自 Go Blog 的帖子.)
给定的 blockReader
类型, 我们可以通过封装输入读取器来取消阻止图像数据流, 如下所示:
deblockingReader := &blockReader{r: imageFile}
这种包装将以块分隔的 GIF 图像流转换为简单的字节流, 可通过调用 blockReader
的 Read
函数访问这些字节流.
连接件
实现 blockReader
并从库中获得 LZW 压缩器后, 我们就可以解码图像数据流了. 我们直接用以下代码以迅雷不及掩耳之势将它们糅合在一起:
lzwr := lzw.NewReader(&blockReader{r: d.r}, lzw.LSB, int(litWidth))
if _, err = io.ReadFull(lzwr, m.Pix); err != nil {
break
}
就是这样.
第一行创建了一个 blockReader
并将其传递给 lzw.NewReader
以创建一个解压缩器。 此处 dr
是保存图像数据的 io.Reader
, lzw.LSB
定义 LZW 解压缩器中的字节顺序, 而 litWidth
是像素深度.
给定解压器后, 第二行调用 io.ReadFull
函数解压数据并将其存储在集合 m.Pix
中. 返回 ReadFull
后, 图像数据将被解压并存储在图像中, 即 m
, 可以随时显示.
这些代码这次可以工作了. 不信你找我哦.
我们可以通过将 NewReader
调用放入 ReadFull
的参数列表中来避免临时变量 lzwr
, 就像我们在内部构建 blockReader
一样对 NewReader
的调用, 但这可能会将太多内容打包到一行代码中.
总结
Go 的接口通过组装这样的零件来重组数据, 使构建软件变得容易. 在此示例中, 我们通过使用 io.Reader
接口将解块器和解压缩器链接在一起来实现 GIF 解码, 类似于类型安全的 Unix 管道. 此外我们将解块器编写为 Reader
接口的(隐式)实现, 然后不需要额外的声明或样板代码即可将其放入处理管道中. 在大多数语言中很难如此紧凑, 简洁, 安全地实现此解码器, 但是接口机制和一些约定使它在 Go 中非常顺溜.
来看看这次的 GIF:
GIT 格式的定义在这里 www.w3.org/Graphics/GIF/spec-gif89a....
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: