FLIC(FLI/FLC)是Autodesk早期推出的轻量动画格式,常用于演示程序与游戏片头。它的核心优势在于帧间压缩:首帧为全量图,后续Delta帧仅保存变化部分。要在C++中正确解析Delta帧压缩数据,必须理解其数据包结构与字节序,并维护前一帧的图像缓冲。

FLIC文件与帧的基本结构
FLIC文件以文件头开头,随后是一系列帧。每一帧都有自己的帧头,其中记录了帧大小、帧类型以及该帧包含的数据包数量。对于FLC格式,文件头中的魔法字为0xAF12;对于FLI格式,则为0xAF11。解析时首先需要以二进制方式打开文件,按小端顺序读取这些头部信息。
Delta帧的类型标识通常是FLI_DELTA(0x07)或FLC_DELTA(0x0C,在FLC中也称FLC_DTA_LC等变体)。与全帧不同,Delta帧不描述整张画面,而是用若干“数据包”描述哪些位置的像素发生了改变。这意味着解码器必须保留上一帧的像素数组,新帧在其基础上做局部修改,而非重新生成。
#include <fstream>
#include <vector>
#include <cstdint>
#pragma pack(push, 1)
struct FlicHeader {
uint32_t size;
uint16_t type;
uint16_t frames;
uint16_t width;
uint16_t height;
uint16_t depth;
uint16_t flags;
uint32_t speed;
// 其余字段省略
};
#pragma pack(pop)
bool isFlicFile(std::ifstream& f) {
FlicHeader h;
f.read(reinterpret_cast<char*>(&h), sizeof(h));
f.seekg(0);
return h.type == 0xAF11 || h.type == 0x0xAF12;
}
Delta帧数据包的分类
Delta帧的主体是由多个数据包依次拼接而成。FLC最常见的Delta包类型包括FLC_DTA_LC(带行压缩的Delta)和FLC_DTA_BR(字节游程编码)。FLI则常用FLI_DELTA中的子结构:先给出跳过行数,再给出当前行内的若干操作。无论哪种变体,本质都是“在哪里写、写多少、写什么”。
以FLC_DTA_LC为例,包内先是一个uint16_t表示后续有多少行数据;对每一行,先有uint16_t的起始列偏移,再有uint16_t的像素计数,然后是对应数量的像素值(通常是16位彩色)。如果像素计数最高位为1,则表示游程:重复下一个像素值(计数取低15位)次。这种混合编码兼顾了零散修改与连续色块。
| 包类型 | 含义 | 典型场景 |
|---|---|---|
| FLC_DTA_LC | 按行给出列偏移与像素 | 局部图形变动 |
| FLC_DTA_BR | 字节级游程编码 | 大面积同色填充 |
| FLI_DELTA | 跳过行+行内操作 | 老式FLI动画 |
C++解析Delta帧的核心步骤
第一步是准备帧缓存。可以用std::vector<uint8_t>保存RGB或RGB565数据,尺寸为宽乘高乘每像素字节数。读取Delta帧时,先读帧头得到包数,然后循环解每个包。对于每一包,依据类型字段分支处理,将解出的像素写入缓存对应偏移。由于Delta依赖前帧,首帧必须优先以全帧方式解码。
第二步是处理字节序与对齐。x86平台本身小端,但若在特殊环境或读取多字节字段时,建议用辅助函数显式转换。数据包内部常使用#pragma pack(1)结构或直接按字节读取,避免编译器填充导致错位。下面给出一个简化版的FLC_DTA_LC行解码片段:
// 假设buf为上一帧缓存,行优先,每像素2字节
void decodeLine(std::vector<uint8_t>& buf, int width, int y,
const uint8_t* data, int dataLen) {
int pos = 0;
while (pos < dataLen) {
uint16_t skip = *(uint16_t*)(data + pos); pos += 2;
uint16_t count = *(uint16_t*)(data + pos); pos += 2;
int x = skip;
if (count & 0x8000) {
int run = count & 0x7FFF;
uint16_t val = *(uint16_t*)(data + pos); pos += 2;
for (int i = 0; i < run; ++i) {
if (x < width) {
int off = (y * width + x) * 2;
buf[off] = val & 0xFF;
buf[off+1] = val >> 8;
}
x++;
}
} else {
for (int i = 0; i < count; ++i) {
if (x < width) {
uint16_t val = *(uint16_t*)(data + pos);
int off = (y * width + x) * 2;
buf[off] = val & 0xFF;
buf[off+1] = val >> 8;
}
pos += 2;
x++;
}
}
}
}
常见错误与边界处理
初学者常犯的错误是每次解码Delta帧都新建空白缓冲,结果只看到零星色块。必须明确:Delta帧是增量更新,解码函数应接收并修改已有缓冲。另一个误区是忽略行跳过值,直接顺序写入,这会让图像在垂直方向错位,表现为撕裂状条纹。
在工程实现中,还应为所有坐标与长度做边界校验。FLIC文件可能损坏或被人篡改,若skip+count超过width,或pos超出包长度,应截断或报错而非越界写入。将解包逻辑封装为独立函数,并配合单元测试构造最小FLC片段,能大幅降低调试成本。
提示:若需将解析结果输出到屏幕,可将帧缓存转换为BGRA并交给SDL或Qt显示;若仅做转码,则直接写PNG序列即可。
完整解码循环示例
下面给出一个更高层的帧循环骨架,展示如何串联文件头、帧头与Delta解包。实际项目中应补充异常捕获与资源释放,此处为突出主线予以简化。
void playFlic(const char* path) {
std::ifstream f(path, std::ios::binary);
FlicHeader h;
f.read(reinterpret_cast<char*>(&h), sizeof(h));
int w = h.width, ht = h.height;
std::vector<uint8_t> frame(w * ht * 2, 0);
for (int i = 0; i < h.frames; ++i) {
uint32_t frameSize; f.read((char*)&frameSize, 4);
uint16_t frameType; f.read((char*)&frameType, 2);
if (frameType == 0x0C) { // FLC_DELTA
uint16_t packets; f.read((char*)&packets, 2);
f.seekg(8, std::ios::cur); // 跳过保留字段
for (int p = 0; p < packets; ++p) {
uint16_t ptype; f.read((char*)&ptype, 2);
uint32_t plen; f.read((char*)&plen, 4);
std::vector<uint8_t> pdata(plen - 6);
f.read((char*)pdata.data(), plen - 6);
if (ptype == 0x0F) { // FLC_DTA_LC
int pos = 0;
uint16_t lines = *(uint16_t*)pdata.data(); pos += 2;
int y = 0;
while (pos < pdata.size() && y < lines) {
uint16_t lc = *(uint16_t*)(pdata.data()+pos); pos += 2;
for (int l = 0; l < lc; ++l) {
decodeLine(frame, w, y, pdata.data()+pos,
pdata.size()-pos);
// 简化:实际需按行长度跳转
}
y++;
}
}
}
}
// 显示或保存frame
}
}
通过上述方式,C++程序可以稳定还原FLIC动画中的Delta帧压缩数据。重点在于理解增量更新本质、正确处理包内游程与偏移,以及始终维护一份有效的上一帧缓存。