操作 GIF 调色板





5.00/5 (21投票s)
在不触碰图像数据本身的情况下编辑 GIF 颜色
引言
我正在为我的另一个项目寻找一些不错的免费动画 GIF。我找到了很多,但它们不符合我的配色方案,所以由于它们可以自由修改,我尝试更改颜色——这对于使用 GIMP 的简单图像来说足够容易,但当涉及到动画 GIF 时,它很快就变成了一个耗时的噩梦:一个简单的动画,100 帧,而使用 GIMP,你一次只能调整一帧的颜色。
所以我开始寻找一个好工具来替我做这件事:什么都没有!我找到的所有工具要么没有达到预期效果,要么完全弄乱了结果。当涉及到透明度时,什么都行不通。所以我最终开始研究 GIF 格式,并决定实现我的目标,这根本没有那么复杂。
背景
所以在这里——就像我在许多有用的网站上遇到的一样——我将简单介绍一下通用的 GIF 格式。GIF 的核心,即存储图像的方法,是使用 LZW(Lempel-Ziv-Welch)算法的 GIF 版本压缩的位图。我没有深入研究这一点,因为颜色存储在独立的颜色表中,这些颜色表未压缩且易于访问。所以,这是 GIF 容器格式的图像概述
现在,让我们以动画 GIF 图像为例,深入了解一下该格式
我在 HEX 编辑器中打开了它并对其进行了着色——我将在下面解释
区域 1:头部信息
前 13 个字节始终是 GIF 文件的头部。
偏移量 | 长度 | style="width: 90px">名称 | 描述 | |||||||||||||||
0x000000 | 6 | 标识 & 版本 | 前 3 个字节始终是“GIF”作为标识符。然后是文件版本:89a 或 87a | |||||||||||||||
0x000006 | 2 | 宽度 | 图像的宽度 - 第一个字节是最低有效位:“01 00”将是 1,而“00 01”将是 256 | |||||||||||||||
0x000008 | 2 | 高度 | 图像的高度 - 第一个字节又是最低有效位 | |||||||||||||||
0x00000A | 1 | 标志 | 此字节在其位中包含一些选项(从左到右,从索引 0 开始,最重要到最不重要 - 因此 0 表示 128,7 表示 1)
| |||||||||||||||
0x00000B | 1 | 背景颜色 | 背景颜色的索引 - 如果未使用全局颜色表,则无关紧要 | |||||||||||||||
0x00000C | 1 | 长宽比 | 这在现在被忽略 |
区域 2:全局颜色表(可选)
这是一个简单的颜色列表,每 3 个字节的格式为 RGB。“可选
”意味着如果 UseGlobalColorTable
标志设置为 0
,则此部分不存在,图像部分从此处开始。字节数可以通过上述方法计算:3 * 2 ^ (GlobalColorTableSize + 1)
。
区域 3:图像部分
在头部之后,如果使用了全局颜色表,则紧接着是一个图像部分列表。
数据区域
要继续,您必须了解 GIF 文件中的数据区域。数据区域以一个字节的 长度定义 开头,后跟该数量的 数据字节。只要长度定义为 0
,该模式就会重复,在这种情况下,数据区域结束。
图像部分类型
有两种主要的图像部分类型,可以通过第一个字节确定
标识符 | 名称 | 描述 |
---|---|---|
0x2C | LWZ 图片 | 这包含具有自身结构的图像数据 |
0x21 | 元数据 | 这包含不同类型的元数据 - 但它们都具有相同的通用结构 |
LWZ 图片
LWZ 图像结构与 GIF 文件本身类似。每个 LWZ 图像部分都有头部信息,并且可以有一个局部颜色表。这是带有相对偏移的结构
偏移量 | 长度 | 名称 | 描述 | ||||||||||||||||||
0x000000 | 1 | 标识符 | LWZ 图像部分标识 0x2C | ||||||||||||||||||
0x000001 | 2 | 左侧 | 此图像在全局图像表面中的水平位置 - 最低有效字节在前 | ||||||||||||||||||
0x000003 | 2 | 顶部 | 此图像在全局图像表面中的垂直位置 - 最低有效字节在前 | ||||||||||||||||||
0x000005 | 2 | 宽度 | 此图像部分的宽度 - 最低有效字节在前 | ||||||||||||||||||
0x000007 | 2 | 高度 | 此图像部分的高度 - 最低有效字节在前 | ||||||||||||||||||
0x000009 | 1 | 标志 | 此字节在其位中包含一些选项(从左到右,从索引 0 开始,最重要到最不重要)
| ||||||||||||||||||
0x00000A | * | 本地颜色表 | 一个简单的 3 字节颜色定义列表(参见全局颜色表),如果 UseLocalColorTable 为 0 ,则此部分不存在 | ||||||||||||||||||
* | 1 | 最小代码大小 | 图像数据中 LZW 码的初始位数 | ||||||||||||||||||
* | * | Data | 包含压缩图像位图的 数据区域 |
元数据
元数据用于控制动画和图像部分属性,这些属性不包含在 LWZ 图像数据中。这些仅包含在 GIF 版本 89a 中——但几乎所有 GIF 图像都是如此。所有元数据类型都具有相同的结构,带有相对偏移
偏移量 | 长度 | 名称 | 描述 |
0x000000 | 1 | 标识符 | 元数据标识 0x21 |
0x000001 | 1 | 子类型 | 元数据类型 |
0x000002 | * | Data | 包含元数据的 数据区域 |
已知有 4 种元数据类型 - 但理论上,可以有任何类型
- 0x01 - 纯文本: 这可以用于在图像上绘制纯文本 - 我还没有尝试过。
数据区域 名称 描述 1 选项
这总是 12 个字节长,并包含以下信息结构 偏移量 长度 名称 描述 0x000000
2
左侧
文本的水平位置 0x000002
2
顶部
文本的垂直位置 0x000004
2
宽度
文本字段的宽度 0x000006
2
高度
文本字段的高度 0x000008
1
字符宽度
字符宽度 0x000009
1
字符高度
字符高度 0x00000A
1
TextColor
文本颜色的索引 0x00000B
1
背景颜色
背景颜色的索引 2-* 文本
这是要绘制的文本——如果超过 255 字节,它将被分割成独立的数据区域 - 0xF9 - 图形控制:这可以用于为以下图像数据添加更多选项
数据区域 名称 描述 1 选项 这始终是 4 个字节长,并包含此信息结构 偏移量 长度 名称 描述 0x000000 1 选项
位 0-2:保留
位 3-5:处置方法
位 6:使用用户输入
位 7:使用透明度
处置方法- 000:未指定
- 001:不处置
- 010:恢复为背景颜色
- 011:恢复到之前
- 1xx:保留
0x000001 2 延迟时间
此帧应显示的时长,单位为 1/100 秒 0x000003 1 透明颜色
透明颜色的索引 - 如果使用透明度为 0,则无关紧要 - 0xFE - 注释:这可以用于在文件中插入注释 - 我还没有尝试过。我猜整个数据区域数据一起构成了注释
- 0xFF - 应用程序:这可以用于存储特定应用程序的信息
数据区域 名称 描述 1 应用标识
这应该总是 11 个字节长。前 8 个字节是应用程序的名称,其余 3 个字节应该构成一种认证。 2-* Data
这是应用程序特定的数据。 如果 AppIdent 是
NETSCAPE2.0
(如示例所示),则 GIF 图像将成为动画,并且应用程序特定数据由 3 个字节组成,其中第一个始终是 01,第二个和第三个字节指定动画重复的次数 - 0 表示永远。
区域 4:终止
GIF 图像的最后一个字节始终是 0x3B。
Using the Code
代码用 C# 编写,并包含在 Visual Studio 2015 解决方案中。
GIF 容器实现
实现 GIF 容器格式的主要类都位于文件 GifWrapper.cs 中
GifWrapper
- 将 GIF 容器实现为GifPart
列表。加载的文件被分割成其各个部分,并在调用GetData()
时重新合并,以便每个部分都可以单独扩展或缩减。GifPartHeader
- 文件的头部 - 如果包含全局颜色表则加上全局颜色表GifPartLzwImage
- 一个带有头部和(如果包含)本地颜色表的 LWZ 压缩图像部分GifPartMetaData
- 一个实现通用元数据部分的通用类 - 以下实现允许访问各个选项GifPartMetaData.TextDraw
- 指定的文本元数据部分GifPartMetaData.GraphicsControl
- 指定的图形控制元数据部分GifPartMetaData.Comment
- 指定的注释元数据部分GifPartMetaData.ApplicationData
- 指定的应用程序数据部分
GifPartTerminator
- 终止符部分(包含 0x3B 字节)GifPartGarbageData
- 文件终止符之后的文件数据
代码使用基于 GifWrapper
类
GifWrapper wrapper;
// You can open files
wrapper = new GifWrapper(@"C:\some\file.gif");
// streams
Stream stream = new FileStream(@"C:\some\file.gif", FileMode.Open);
wrapper = new GifWrapper(stream);
// or a byte array
byte[] data = File.ReadAllBytes(@"C:\some\file.gif");
wrapper = new GifWrapper(data);
// You can then loop through the parts
foreach (GifPart part in wrapper) {
// Then see what type of part it is
if (part is GifPartHeader) {
Console.WriteLine("Size: {0}x{1}",
(part as GifPartHeader).Width, (part as GifPartHeader).Height);
}
}
GifPartHeader
和 GifPartLwzImage
都包含一个属性来访问颜色表。每个都可以设置为 null
来删除颜色表,并使用颜色列表来设置新的颜色表。设置颜色表时,Use***ColorTable
和 ColorTableSize
属性会自动设置。
每个属性都实现为直接更改底层数据。
用户界面
主窗口是 GifWrapperFrm
并实现了
- 带有类型图标的部分列表
- 预览
- 部分实现窗格
- 文件加载/保存和拖放
- 批量更改延迟时间
在子文件夹 GifWrapperCtrls 中,有独立的 UserControl
实现了许多类型 GifPart
的 UI
GifPartGeneralHexViewCtrl
- 所有只包含数据字节作为内容的部分,只以 HEX 视图显示数据,没有编辑功能:GifPartMetaData.Comment
、GifPartMetaData
和GifPartGarbageData
GifPartHeaderCtrl
-GifPartHeader
的可编辑控件,包含一个GifColorTableCtrl
用于编辑颜色表GifPartLwzImageCtrl
-GifPartLwzImage
的可编辑控件,包含一个GifColorTableCtrl
用于编辑颜色表GifPartMetaApplicationCtrl
-GifPartMetaData.ApplicationControl
的可编辑控件,如果AppIdentifier
不是 "NETSCAPE2.0
"GifPartMetaGraphicsControlCtrl
-GifPartMetaData.GraphicsControl
的可编辑控件GifPartMetaLoopCtrl
-GifPartMetaData.ApplicationControl
的可编辑控件,如果 AppIdentifier 是 "NETSCAPE2.0
"GifPartMetaTextCtrl
-GifPartMetaData.TextDraw
的可编辑控件- 没有针对
GifPartTerminator
的控制
可编辑控件中的任何更改都会导致预览图像立即重新加载包含更改的内容,除非更改中存在异常。 导致 GIF 文件解析器错误的无效选项值会在预览图像中显示错误消息作为 | ![]() |
关注点
GifPartLwzImage
包含一个由 Image
提供的基本构造函数。它的工作方式是使用 C# API,将图像保存为 GIF,并将所需的 GIF 图像部分提取到新的 GifPartLwzImage
实例中。因此效率较低。
历史
缺少改进
- 在列表中移动
GifPart
- 包含新的
GifPart
- 颜色表范围内的操作(亮度、对比度、色相等)
- 更好的选项验证
版本 1.1
- Bug 修复:“另存为…”菜单功能只能覆盖现有文件
- 已添加:颜色表的亮度、对比度和颜色(色相)的颜色转换