一部のゲームファイルからPNGを抽出すると、画像が途中で歪んでいることに気付きました。たとえば、SkyrimのTexturesファイルから抽出されたいくつかのPNGを次に示します。
これはPNG形式の異常なバリエーションですか?このようなPNGを適切に表示するには、どのような修正が必要ですか?
一部のゲームファイルからPNGを抽出すると、画像が途中で歪んでいることに気付きました。たとえば、SkyrimのTexturesファイルから抽出されたいくつかのPNGを次に示します。
これはPNG形式の異常なバリエーションですか?このようなPNGを適切に表示するには、どのような修正が必要ですか?
回答:
ティルベルグのさらなる研究のおかげで、ここに「復元された」画像があります。
予想どおり、約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
、2番目の画像からbyte を削除し、次にbyte を削除したときに得られるものです0x8092
。
画像を実際に「修復」するわけではありません。これは試行錯誤で行いました。ただし、それは、16384バイトごとに予期しないデータがあることを示しています。私の推測では、画像はある種のファイルシステム構造にパックされており、予期しないデータは単にデータを読み取るときに削除する必要があるブロックマーカーです。
ブロックマーカーの正確な位置とサイズはわかりませんが、ブロックサイズ自体は2 ^ 14バイトです。
画像の直前と直後に表示されるものの16進ダンプ(数十バイト)も提供できれば助かります。これにより、ブロックの最初または最後に保存される情報の種類に関するヒントが得られます。
もちろん、抽出コードにバグがある可能性もあります。ファイル操作に16384バイトのバッファーを使用している場合は、最初にチェックします。
Samの提案に基づいて、私はJamesのコードをhttps://github.com/tillberg/skyrimで分岐し、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
(Pythonでstring.find()を使用してバイトをカウントすることにより)IDATチャンクとIENDチャンク間の実際の距離を測定すると、暗黙の実際の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)
削除される5バイトセグメントは次のとおりです。
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バイトセグメントの最後の5バイトを含めました。
16 KBごとではなく、〜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 Compressed Data Format Specification)、セクション3.2.4です。
バイナリモードではなく、テキストモードでファイルからデータを読み込んでいる可能性はありますか?
libpng
Skyrim PNGを読むなどの実績のあるPNGローダーを実行しますか?つまり、PNGローダーのバグですか?