Warum werden einige aus Spielen extrahierte PNG-Dateien falsch angezeigt?

14

Mir ist aufgefallen, dass beim Extrahieren von PNGs aus einigen Spieledateien das Bild teilweise verzerrt wird. Hier sind zum Beispiel einige PNGs, die aus der Textures-Datei in Skyrim extrahiert wurden:

Beleuchtetes J PNG von Skyrim Beleuchtetes K PNG von Skyrim

Ist dies eine ungewöhnliche Variation eines PNG-Formats? Welche Änderungen müsste ich vornehmen, um solche PNGs korrekt anzuzeigen?

James Tauber
quelle
1
Vielleicht haben sie eine spezielle Kodierung in ihre Dateien eingefügt, um Leute davon abzuhalten, solche Dinge zu tun. Oder vielleicht funktioniert das, was Sie zum Extrahieren verwenden, nicht richtig.
Richard Marskell - Drackir
Vielleicht ist es eine Art Komprimierung, um die Bilder in der Dateigröße zu verkleinern. Dies geschieht auch in iPhone-Apps.
rechts
1
Ein bisschen abseits vom Thema, aber ist das ein Pony?
JCORA

Antworten:

22

Hier sind die "restaurierten" Bilder, dank der weiteren Recherche von tillberg:

final1 final2

Wie erwartet gibt es alle ca. 0x4020 Bytes einen 5-Byte-Block-Marker . Das Format scheint wie folgt zu sein:

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 */
};

Nachdem der Marker gelesen wurde, bilden die nächsten marker.lenBytes einen Block, der Teil der Datei ist. marker.notlenist eine Regelgröße, so dass marker.len + marker.notlen == 0xffff. Der letzte Block ist so, dass marker.tag == 1.

Die Struktur ist wahrscheinlich wie folgt. Es gibt noch unbekannte Werte.

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? */
};

Ich habe nicht herausgefunden, was am Ende ist, aber da PNGs das Auffüllen akzeptieren, ist es nicht zu dramatisch. Die verschlüsselte Dateigröße zeigt jedoch deutlich an, dass die letzten 4 Bytes ignoriert werden sollten ...

Da ich kurz vor Beginn der Datei nicht auf alle Blockmarkierungen zugreifen konnte, habe ich diesen Decoder geschrieben, der am Ende beginnt und versucht, die Blockmarkierungen zu finden. Es ist überhaupt nicht robust, aber gut, es hat für Ihre Testbilder funktioniert:

#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;
}

Ältere Forschung

Dies erhalten Sie, wenn Sie das Byte 0x4022aus dem zweiten Image entfernen und dann das Byte entfernen0x8092 :

Original erster Schritt zweiter Schritt

Die Bilder werden nicht wirklich „repariert“. Ich habe das durch Ausprobieren gemacht. Es wird jedoch darauf hingewiesen, dass alle 16384 Byte unerwartete Daten vorliegen. Ich vermute, dass die Bilder in einer Art Dateisystemstruktur gepackt sind und die unerwarteten Daten einfach Blockmarkierungen sind , die Sie beim Lesen der Daten entfernen sollten.

Ich weiß nicht genau, wo sich die Blockmarkierungen befinden und wie groß sie sind, aber die Blockgröße selbst beträgt mit Sicherheit 2 ^ 14 Bytes.

Es wäre hilfreich, wenn Sie auch einen hexadezimalen Speicherauszug (ein paar Dutzend Bytes) dessen bereitstellen könnten, was direkt vor dem Bild und direkt danach angezeigt wird. Dies würde Hinweise darauf geben, welche Informationen am Anfang oder Ende der Blöcke gespeichert sind.

Natürlich gibt es auch die Möglichkeit, dass Ihr Extraktionscode einen Fehler enthält. Wenn Sie einen Puffer von 16384 Bytes für Ihre Dateioperationen verwenden, würde ich zuerst dort überprüfen.

sam hocevar
quelle
+1 sehr hilfreich; Ich werde mich weiter mit dem Hinweis befassen, den Sie mir gegeben haben, und einige zusätzliche Informationen veröffentlichen
James Tauber,
Die eingebettete "Datei" beginnt mit einer Zeichenfolge mit Längenpräfix, die den Dateinamen enthält. gefolgt von 12 Bytes vor der 89 50 4e 47 Magie für PNG-Dateien. Die 12 Bytes sind: 40 25 01 00 78 9c 00 2a 40 d5 bf
James Tauber
Gute Arbeit, Sam. Ich habe den Python-Code aktualisiert, der die BSA-Dateien direkt liest, um dasselbe zu tun. Die Ergebnisse sind unter orbza.s3.amazonaws.com/tillberg/pics.html sichtbar (ich zeige dort nur 1/3 der Bilder, gerade genug, um die Ergebnisse zu demonstrieren). Dies funktioniert bei vielen Bildern. Es gibt einige andere Dinge, die mit einigen der anderen Bilder vor sich gehen. Ich frage mich, ob dies anderswo in Fallout 3 oder Skyrim behoben wurde.
Tillberg
Hervorragende Arbeit, Jungs! Ich werde auch meinen Code aktualisieren
James Tauber
18

Auf Sams Vorschlag hin gab ich den Code von James unter https://github.com/tillberg/skyrim heraus und konnte n_letter.png erfolgreich aus der Skyrim Textures BSA-Datei extrahieren.

Der Buchstabe N

Die von den BSA-Headern angegebene "file_size" ist nicht die tatsächliche endgültige Dateigröße. Es enthält einige Header-Informationen sowie einige zufällige Datenblöcke, die überflüssig wirken und verstreut sind.

Die Überschriften sehen ungefähr so ​​aus:

  • 1 Byte (Länge des Dateipfads?)
  • Der vollständige Pfad der Datei, ein Byte pro Zeichen
  • 12 Bytes unbekannter Herkunft, wie von James veröffentlicht (40 25 01 00 78 9c 00 2a 40 d5 bf).

So entfernen Sie die Header-Bytes:

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

Von dort aus startet die eigentliche PNG-Datei. Das lässt sich anhand der 8-Byte-Startsequenz von PNG leicht überprüfen.

Ich versuchte herauszufinden, wo sich die zusätzlichen Bytes befanden, indem ich die PNG-Header las und die im IDAT-Block übergebene Länge mit der implizierten Datenlänge verglich, die aus der Messung der Anzahl der Bytes bis zum IEND-Block abgeleitet wurde. (Details dazu finden Sie in der Datei bsa.py bei github)

Die durch die Chunks in n_letter.png angegebenen Größen sind:

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

Als ich den tatsächlichen Abstand zwischen dem IDAT-Chunk und dem darauf folgenden IEND-Chunk gemessen habe (indem ich die Bytes mit string.find () in Python gezählt habe), stellte ich fest, dass die tatsächliche implizierte IDAT-Länge 60640 Bytes betrug - es gab zusätzliche 15 Bytes darin .

Im Allgemeinen enthielten die meisten "Brief" -Dateien 5 zusätzliche Bytes pro 16 KB Gesamtdateigröße. Zum Beispiel hatte o_letter.png bei ca. 73 KB zusätzliche 20 Bytes. Größere Dateien, wie die arkanen Kritzeleien, folgten größtenteils demselben Muster, obwohl bei einigen ungerade Mengen hinzugefügt wurden (52 Bytes, 12 Bytes oder 32 Bytes). Keine Ahnung was da los ist.

Für die Datei n_letter.png konnte ich die richtigen Offsets (meist durch Ausprobieren) finden, um die 5-Byte-Segmente zu entfernen.

index = 0x403b
index2 = 0x8070
index3 = 0xc0a0
pngdata = (
  d[0      : (index - 5)] + 
  d[index  : (index2 - 5)] + 
  d[index2 : (index3 - 5)] + 
  d[index3 : ] )
pngfile.write(pngdata)

Die entfernten fünf Bytesegmente sind:

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

Für das, was es wert ist, habe ich die letzten fünf Bytes des unbekannten 12-Byte-Segments aufgenommen, da es Ähnlichkeiten mit den anderen Sequenzen gibt.

Es stellt sich heraus, dass sie nicht alle 16 KB groß sind, sondern in Intervallen von ~ 0x4030 Byte.

Um zu verhindern, dass die oben genannten Indizes nahe beieinander liegen, habe ich auch die Zlib-Dekomprimierung des IDAT-Chunks aus dem resultierenden PNG getestet.

tillberg
quelle
das "1 Byte für ein zufälliges @ -Zeichen" ist die Länge des Dateinamens, glaube ich
James Tauber
Was ist der Wert der jeweils 5-Byte-Segmente?
James Tauber
Ich habe meine Antwort mit hexadezimalen Werten der entfernten 5-Byte-Segmente aktualisiert. Außerdem hatte ich mich über die Anzahl der 5-Byte-Segmente vertauscht (ich zählte zuvor den mysteriösen 12-Byte-Header als 7-Byte-Header und 5-Byte-Wiederholungsteiler). Das habe ich auch behoben.
Tillberg
Beachten Sie, dass (Little-Endian) 0x402A, 0x4030, 0x402B in diesen 5-Byte-Segmenten angezeigt werden. sind sie die tatsächlichen Intervalle?
James Tauber
Ich dachte, ich hätte bereits gesagt, dass dies eine hervorragende Arbeit ist, aber anscheinend habe ich es nicht getan. Ausgezeichnete Arbeit! :-)
sam hocevar
3

Tatsächlich sind die intermittierenden 5 Bytes Teil der Zlib-Komprimierung.

Wie unter http://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/ beschrieben ,

01 Die Little-Endian-Bitfolge 1 00 00000. 1, die den letzten Block angibt, 00, die einen nicht komprimierten Block angibt, und 00000 sind 5 Auffüllbits, um den Anfang eines Blocks an einem Oktett auszurichten (was für nicht komprimierte Blöcke erforderlich ist) und sehr praktisch für mich). 05 00 fa ff Die Anzahl der Datenoktette im unkomprimierten Block (5). Gespeichert als Little-Endian-16-Bit-Ganzzahl, gefolgt von ihrem 1-Komplement (!).

.. so zeigt eine 00 einen 'nächsten' Block an (kein endender), und die 4 nächsten Bytes sind die Blocklänge und ihre Inverse.

[Bearbeiten] Eine zuverlässigere Quelle ist natürlich RFC 1951 (Deflate Compressed Data Format Specification), Abschnitt 3.2.4.

Jongware
quelle
1

Ist es möglich, dass Sie die Daten aus der Datei in einem Textmodus lesen (in dem Zeilenenden, die in den PNG-Daten erscheinen, möglicherweise unkenntlich gemacht werden), anstatt in einem Binärmodus?

Greg Hewgill
quelle
1
Ja. Das hört sich sehr nach dem Thema an. In Anbetracht dessen ist der Code, der es liest: github.com/jtauber/skyrim/blob/master/bsa.py --- bestätigt :-)
Armin Ronacher
Nein, macht keinen Unterschied.
James Tauber
@JamesTauber, wenn Sie wirklich Ihren eigenen PNG-Loader codieren, wie Armins Kommentar zu implizieren scheint, funktioniert dies dann (a) mit anderen PNGs, die Sie ausprobiert haben, und (b) mit einem bewährten PNG-Loader, wie zum Beispiel libpngden Skyrim-PNGs? Mit anderen Worten, ist es nur ein Fehler in Ihrem PNG-Loader?
Nathan Reed
@ NathanReed alles, was ich tue, ist das Extrahieren des Byte-Streams und das Hochladen hier; Es ist kein "Lader" beteiligt
James Tauber
3
-1, das kann nicht der Grund sein. Wenn die PNG-Dateien auf diese Weise beschädigt würden, gäbe es beim Aufpumpen CRC-Fehler weit vor den Fehlern bei der Bilddecodierung. Abgesehen von dem erwarteten CRLF im Header gibt es in den Dateien auch keine Vorkommen von CRLF.
Sam Hocevar