Чому деякі файли PNG, вилучені з ігор, відображаються неправильно?


14

Я помітив видобуток PNG з деяких ігрових файлів, через які зображення спотворюється частково. Наприклад, ось пара PNG, витягнута з файлу Textures у Skyrim:

Світлова J PNG від Skyrim Світлова К PNG від Skyrim

Це якась незвичайна різниця у форматі PNG? Які зміни мені потрібно внести для правильного перегляду таких PNG?


1
Можливо, вони вставили у свої файли якесь спеціальне кодування, щоб люди не могли робити подібні речі. А може все, що ви використовуєте для витягу, не працює належним чином.
Річард Марскелл - Дракір

Можливо, це свого роду стиснення, щоб зменшити зображення у розмірі файлів. Це також робиться в додатках для iPhone.
праворуч

1
Трохи поза темою, але це поні?
jcora

Відповіді:


22

Ось «відновлені» зображення, завдяки подальшим дослідженням Донберга:

остаточний1 заключний2

Як і очікувалося, є 5-байтовий блоковий маркер кожні приблизно 0x4020 байт. Формат видається таким:

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з другого зображення, потім видаляючи байт 0x8092:

оригінальний Перший крок другий крок

Він насправді не «ремонтує» зображення; Я зробив це шляхом спроб та помилок. Однак, це говорить про те, що кожні 16384 байти є несподіваними даними. Я здогадуюсь, що зображення упаковані у якусь структуру файлової системи, а несподівані дані - це просто блокувати маркери, які слід видалити під час читання даних.

Я не знаю, де саме знаходяться маркери блоків і їх розмір, але сам розмір блоку, безумовно, становить 2 ^ 14 байт.

Це допоможе, якщо ви також можете надати шістнадцятковий дамп (кілька десятків байтів) того, що з’являється прямо перед зображенням та відразу після нього. Це дасть підказки про те, яка інформація зберігається на початку або в кінці блоків.

Звичайно, існує ймовірність появи помилки у вашому коді вилучення. Якщо ви використовуєте буфер 16384 байтів для ваших файлових операцій, я спершу там перевірю.


+1 дуже корисно; Я продовжую розбиратися в цьому разом із головою, яку ви мені дали, і опублікую додаткову інформацію
Джеймс Таубер

Вбудований "файл" починається з попередньо встановленої довжини рядка, що містить ім'я файлу; далі 12 байт перед магією 89 50 4e 47 для файлів PNG. 12 байт: 40 25 01 00 78 9c 00 2a 40 d5 bf
Джеймс Таубер

Гарна робота, Сем. Я оновив код python, який насправді читає файли BSA, щоб зробити те саме. Результати видно на orbza.s3.amazonaws.com/tillberg/pics.html (я показую лише 1/3 зображень там, достатньо, щоб продемонструвати результати). Це працює для багатьох зображень. З деякими іншими образами відбуваються деякі інші речі. Мені цікаво, чи це було вирішено в інших місцях, повторно в Fallout 3 або Skyrim.
доберг

Відмінна робота, хлопці! Я також оновлю код
Джеймс Таубер

18

На підставі пропозиції Сема, я відправив код Джеймса за адресою https://github.com/tillberg/skyrim і зміг успішно витягнути n_letter.png з файлу BSA Skyrim Textures.

Буква N

"Розмір файлу", заданий заголовками BSA, не є фактичним кінцевим розміром файлу. Вона включає в себе деяку інформацію заголовка, а також деякі випадкові шматки марних, здавалося б, даних, розкиданих навколо.

Заголовки виглядають приблизно так:

  • 1 байт (довжина шляху файлу?)
  • повний шлях до файлу, один байт на символ
  • 12 байт невідомого походження, як розмістив Джеймс (40 25 01 00 78 9c 00 2a 40 d5 bf).

Щоб зняти байти заголовка, я зробив це:

f.seek(file_offset)
data = f.read(file_size)
header_size = 1 + len(folder_path) + len(filename) + 12
d = data[header_size:]

Звідси починається власне файл PNG. Перевірити це можна з 8-байтової послідовності PNG.

Я продовжував намагатися з'ясувати, де знаходяться зайві байти, читаючи заголовки PNG і порівнюючи довжину, пройдену в купі IDAT, і довжину мається на увазі, що визначається від вимірювання кількості байтів до фрагменту IEND. (для детальної інформації про це перегляньте файл bsa.py в github)

Розміри, задані шматками в n_letter.png:

IHDR: 13 bytes
pHYs: 9 bytes
iCCP: 2639 bytes
cHRM: 32 bytes
IDAT: 60625 bytes
IEND: 0 bytes

Коли я виміряв фактичну відстань між купою IDAT та відрізком IEND після нього (підрахувавши байти за допомогою string.find () у Python), я виявив, що фактична довжина IDAT, що мається на увазі, становила 60640 байт - там було додатково 15 байт .

Загалом, більшість «буквених» файлів мали додаткові 5 байт на кожні 16 КБ загального розміру файлу. Наприклад, o_letter.png, близько 73 КБ, мав додаткові 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-байтового сегмента через деяку схожість з іншими послідовностями.

Виявляється, вони не зовсім кожні 16 КБ, але з інтервалом ~ 0x4030 байт.

Щоб уникнути отримання близьких, але не досконалих відповідностей у вищезазначених показниках, я також протестував декомпресію зліб куски IDAT з отриманого PNG, і він проходить.


"1 байт для випадкового знака @" - це довжина рядка імені файлу, я вважаю,
Джеймс Таубер

яке значення 5-байтних сегментів у кожному випадку?
Джеймс Таубер

Я оновив свою відповідь шістнадцятковими значеннями видалених 5-байтних сегментів. Крім того, я змішав себе за кількістю 5-байтних сегментів (я раніше рахував загадковий 12-байтний заголовок як 7-байтний заголовок і 5-байтний повторювальний роздільник). Я це теж виправив.
доберг

зауважте, що (малопомітні) 0x402A, 0x4030, 0x402B з'являються в цих 5-байтних сегментах; це фактичні інтервали?
Джеймс Таубер

Я думав, що вже сказав, що це була відмінна робота, але, мабуть, я цього не зробив. Відмінна робота! :-)
sam hocevar

3

Власне, переривчасті 5 байт є частиною стиснення zlib.

Докладніше про http://drj11.wordpress.com/2007/11/20/a-use-for-uncompression-pngs/ ,

01 маленький ендіанський бітовий рядок 1 00 00000. 1, що вказує на кінцевий блок, 00 вказує на нестиснений блок, а 00000 - 5 біт накладки для вирівнювання початку блоку по октету (що потрібно для нестиснених блоків , і дуже зручно для мене). 05 00 fa ff Кількість октетів даних у нестисненому блоці (5). Зберігається як 16-бітове ціле число з невеликим терміном, а його 1-е доповнення (!).

.. таким чином, 00 вказує на блок "наступний" (не закінчується), а 4 наступні байти - це довжина блоку та його обернена.

[Редагувати] Більш надійним джерелом є, звичайно, RFC 1951 (Дефляція стислих форматів стислих даних), розділ 3.2.4.


1

Чи можливо, ви читаєте дані з файлу в текстовому режимі (де закінчення рядків, які трапляються у даних PNG, можливо, невідомі) замість двійкового режиму?


1
Ага. Це дуже схоже на проблему. Зважаючи на це код, який його читає: github.com/jtauber/skyrim/blob/master/bsa.py --- підтверджено :-)
Armin Ronacher

Ні, не має значення.
Джеймс Таубер

@JamesTauber, якщо ви дійсно кодуєте свій власний завантажувач PNG, як здається, коментар Armin, а) чи працює він на інших PNG-файлах, які ви пробували, і (b) чи перевірений завантажувач PNG, такий як libpngчитання PNG Skyrim? Іншими словами, це просто помилка у вашому завантажувачі PNG?
Натан Рід

@NathanReed - все, що я роблю, - це витягнути потік байтів і завантажити його сюди; немає жодного "навантажувача"
Джеймс Таубер

3
-1, це не може бути причиною. Якщо файли PNG були пошкоджені таким чином, на стадії надуття зафіксовано помилки CRC, перш ніж помилки на етапі декодування зображення. Крім того, у файлах немає випадків виникнення CRLF, окрім очікуваного у заголовку.
sam hocevar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.