我注意到从某些游戏文件中提取PNG时,图像会在途中失真。例如,以下是从Skyrim中的Textures文件中提取的几个PNG:
这是PNG格式的一些异常变化吗?我需要进行哪些修改才能正确查看此类PNG?
我注意到从某些游戏文件中提取PNG时,图像会在途中失真。例如,以下是从Skyrim中的Textures文件中提取的几个PNG:
这是PNG格式的一些异常变化吗?我需要进行哪些修改才能正确查看此类PNG?
Answers:
这是经过得力伯格进一步研究后的“还原”图像:
如预期的那样,每大约0x4020字节有一个5字节的块标记。格式如下所示:
struct marker {
uint8_t tag; /* 1 if this is the last marker in the file, 0 otherwise */
uint16_t len; /* size of the following block (little-endian) */
uint16_t notlen; /* 0xffff - len */
};
读取标记后,接下来的marker.len
字节将构成文件的一部分。marker.notlen
是这样的控制变量marker.len + marker.notlen == 0xffff
。最后一个块是这样的marker.tag == 1
。
结构可能如下。仍然有未知值。
struct file {
uint8_t name_len; /* number of bytes in the filename */
/* (not sure whether it's uint8_t or uint16_t) */
char name[name_len]; /* filename */
uint32_t file_len; /* size of the file (little endian) */
/* eg. "40 25 01 00" is 0x12540 bytes */
uint16_t unknown; /* maybe a checksum? */
marker marker1; /* first block marker (tag == 0) */
uint8_t data1[marker1.len]; /* data of the first block */
marker marker2; /* second block marker (tag == 0) */
uint8_t data2[marker2.len]; /* data of the second block */
/* ... */
marker lastmarker; /* last block marker (tag == 1) */
uint8_t lastdata[lastmarker.len]; /* data of the last block */
uint32_t unknown2; /* end data? another checksum? */
};
我还没有弄清楚到底是什么,但是由于PNG接受填充,所以它不太戏剧化。但是,编码后的文件大小清楚地表明应忽略最后4个字节...
由于在文件开始之前我无法访问所有块标记,因此我编写了从结尾处开始的解码器,并尝试查找块标记。它一点也不健壮,但是很好,它适用于您的测试图像:
#include <stdio.h>
#include <string.h>
#define MAX_SIZE (1024 * 1024)
unsigned char buf[MAX_SIZE];
/* Usage: program infile.png outfile.png */
int main(int argc, char *argv[])
{
size_t i, len, lastcheck;
FILE *f = fopen(argv[1], "rb");
len = fread(buf, 1, MAX_SIZE, f);
fclose(f);
/* Start from the end and check validity */
lastcheck = len;
for (i = len - 5; i-- > 0; )
{
size_t off = buf[i + 2] * 256 + buf[i + 1];
size_t notoff = buf[i + 4] * 256 + buf[i + 3];
if (buf[i] >= 2 || off + notoff != 0xffff)
continue;
else if (buf[i] == 1 && lastcheck != len)
continue;
else if (buf[i] == 0 && i + off + 5 != lastcheck)
continue;
lastcheck = i;
memmove(buf + i, buf + i + 5, len - i - 5);
len -= 5;
i -= 5;
}
f = fopen(argv[2], "wb+");
fwrite(buf, 1, len, f);
fclose(f);
return 0;
}
这是0x4022
从第二个图像中删除字节,然后通过删除byte时得到的0x8092
:
它并没有真正“修复”图像;我通过反复试验做到了这一点。但是,它告诉我们每16384个字节有意外数据。我的猜测是,图像打包在某种文件系统结构中,意外数据只是块标记读取数据时应删除的。
我不知道块标记的确切位置和大小,但是块大小本身肯定是2 ^ 14字节。
如果您还可以提供十六进制转储(几十个字节),该十六进制转储显示在映像之前和之后的内容。这将提示有关在块的开头或结尾存储了哪种信息。
当然,提取代码中也有可能存在错误。如果您使用16384字节的缓冲区进行文件操作,那么我将首先在此处进行检查。
根据Sam的建议,我在https://github.com/tillberg/skyrim上分叉了James的代码并能够从Skyrim Textures BSA文件中成功提取n_letter.png。
BSA标头提供的“ file_size”不是实际的最终文件大小。它包括一些标头信息以及一些散乱的无用的看起来数据的随机块。
标头看起来像这样:
为了剥离头字节,我这样做:
f.seek(file_offset)
data = f.read(file_size)
header_size = 1 + len(folder_path) + len(filename) + 12
d = data[header_size:]
从那里开始,实际的PNG文件开始。很容易从PNG 8字节开始序列中进行验证。
我继续尝试通过读取PNG标头并比较IDAT块中传递的长度与从测量IEND块之前的字节数得出的隐含数据长度来找出多余的字节在哪里。(有关详细信息,请查看github上的bsa.py文件)
n_letter.png中的块给出的大小为:
IHDR: 13 bytes
pHYs: 9 bytes
iCCP: 2639 bytes
cHRM: 32 bytes
IDAT: 60625 bytes
IEND: 0 bytes
当我测量IDAT块和IEND块之间的实际距离时(通过在Python中使用string.find()计数字节),我发现暗示的实际IDAT长度为60640字节-那里还有15字节。
通常,大多数“字母”文件每增加16KB文件大小,就会多出5个字节。例如,大约73KB的o_letter.png具有额外的20个字节。较大的文件(例如,奥秘的乱写)大多遵循相同的模式,尽管有些文件添加了奇数数量(52字节,12字节或32字节)。不知道那是怎么回事。
对于n_letter.png文件,我能够找到要删除5字节段的正确偏移量(主要是通过反复试验)。
index = 0x403b
index2 = 0x8070
index3 = 0xc0a0
pngdata = (
d[0 : (index - 5)] +
d[index : (index2 - 5)] +
d[index2 : (index3 - 5)] +
d[index3 : ] )
pngfile.write(pngdata)
删除的五个字节段为:
at 000000: 00 2A 40 D5 BF (<-- included at end of 12 bytes above)
at 00403B: 00 30 40 CF BF
at 008070: 00 2B 40 D4 BF
at 00C0A0: 01 15 37 EA C8
对于它的价值,由于与其他序列有些相似,我包括了未知的12字节段的最后五个字节。
事实证明,它们并不是每个16KB,而是〜0x4030字节间隔。
为了防止在上面的索引中获得接近但不完美的匹配,我还测试了从生成的PNG中对IDAT块进行zlib解压缩,然后将其通过。
实际上,间歇的5个字节是zlib压缩的一部分。
如http://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/中所述,
01小尾数位字符串1 0000000。1表示最后一个块,00表示一个非压缩块,而00000是5个填充位,用于将一个块的开头对齐到八位位组(非压缩块需要此位) ,对我来说非常方便)。05 00 fa ff未压缩块(5)中数据的八位位组数。存储为小尾数16位整数,后跟1的补码(!)。
..因此,00表示“下一个”块(不是末尾的块),接下来的4个字节是该块的长度及其倒数。
[编辑]当然,更可靠的来源是RFC 1951(Deflate压缩数据格式规范)的3.2.4节。
您是否有可能以文本模式(其中碰巧出现在PNG数据中的行尾可能会被扭曲)而不是二进制模式从文件中读取数据?
libpng
读取Skyrim PNG?换句话说,这只是您的PNG加载器中的错误吗?