Perché alcuni file PNG estratti dai giochi vengono visualizzati in modo errato?


14

Ho notato che estraendo PNG da alcuni file di gioco l'immagine viene distorta a metà. Ad esempio, ecco un paio di PNG estratti dal file Textures in Skyrim:

PNG illuminato di Skyrim K PNG illuminato di Skyrim

È una variazione insolita in un formato PNG? Quali modifiche dovrei fare per visualizzare correttamente questi PNG?


1
Forse hanno inserito una codifica speciale nei loro file per impedire alle persone di fare cose del genere. O forse qualunque cosa tu stia usando per estrarre non funziona correttamente.
Richard Marskell - Drackir,

Forse è una specie di compressione per rendere le immagini più piccole in dimensioni file. Questo viene fatto anche nelle app per iPhone.
destra

1
Un po 'fuori tema, ma è un pony?
jcora,

Risposte:


22

Ecco le immagini “restaurate”, grazie alle ulteriori ricerche di tillberg:

def.1 final2

Come previsto, è presente un marker di blocco a 5 byte ogni circa 0x4020 byte. Il formato sembra essere il seguente:

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

Dopo aver letto l'indicatore, i marker.lenbyte successivi formano un blocco che fa parte del file. marker.notlenè una variabile di controllo tale che marker.len + marker.notlen == 0xffff. L'ultimo blocco è tale marker.tag == 1.

La struttura è probabilmente la seguente. Ci sono ancora valori sconosciuti.

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

Non ho capito cosa ci sia alla fine, ma poiché i PNG accettano il padding, non è troppo drammatico. Tuttavia, la dimensione del file codificato indica chiaramente che gli ultimi 4 byte devono essere ignorati ...

Dato che non avevo accesso a tutti i marcatori di blocchi appena prima dell'inizio del file, ho scritto questo decodificatore che inizia alla fine e tenta di trovare i marcatori di blocco. Non è affatto robusto ma bene, ha funzionato per le immagini di prova:

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

Ricerche precedenti

Questo è ciò che ottieni quando rimuovi byte 0x4022dalla seconda immagine, quindi rimuovendo byte 0x8092:

originale primo passo Secondo passo

In realtà non "ripara" le immagini; L'ho fatto per tentativi ed errori. Tuttavia, ciò che dice è che ci sono dati imprevisti ogni 16384 byte. La mia ipotesi è che le immagini siano racchiuse in una sorta di struttura del filesystem e che i dati imprevisti siano semplicemente dei marker di blocco che dovresti rimuovere durante la lettura dei dati.

Non so dove siano esattamente i marker di blocco e la loro dimensione, ma la dimensione del blocco stesso è sicuramente 2 ^ 14 byte.

Sarebbe utile se si potesse anche fornire un dump esadecimale (alcune dozzine di byte) di ciò che appare subito prima dell'immagine e subito dopo. Ciò darebbe suggerimenti su quale tipo di informazione è memorizzata all'inizio o alla fine dei blocchi.

Naturalmente c'è anche la possibilità che ci sia un bug nel tuo codice di estrazione. Se stai usando un buffer di 16384 byte per le tue operazioni sui file, per prima cosa controllerei lì.


+1 molto utile; Continuerò a scavare in questo con il lead che mi hai dato e pubblicherò alcune informazioni aggiuntive
James Tauber,

Il "file" incorporato inizia con una stringa con prefisso di lunghezza contenente il nome del file; seguito da 12 byte prima della magia 89 50 4e 47 per i file PNG. I 12 byte sono: 40 25 01 00 78 9c 00 2a 40 d5 bf
James Tauber

Bel lavoro, Sam. Ho aggiornato il codice Python che in realtà legge direttamente i file BSA per fare lo stesso. I risultati sono visibili su orbza.s3.amazonaws.com/tillberg/pics.html (sto mostrando solo 1/3 delle immagini lì, quanto basta per dimostrare i risultati). Questo funziona per molte delle immagini. Ci sono alcune altre cose in corso con alcune delle altre immagini. Mi chiedo se questo è stato risolto altrove in Fallout 3 o Skyrim, però.
tillberg,

Ottimo lavoro, ragazzi! Aggiornerò anche il mio codice
James Tauber

18

Sulla base del suggerimento di Sam, ho modificato il codice di James su https://github.com/tillberg/skyrim e sono stato in grado di estrarre con successo n_letter.png dal file BSA di Skyrim Textures.

La lettera n

Il "file_size" fornito dalle intestazioni BSA non corrisponde alla dimensione del file finale effettiva. Include alcune informazioni di intestazione e alcuni pezzi casuali di dati apparentemente inutili sparsi.

Le intestazioni hanno un aspetto simile al seguente:

  • 1 byte (lunghezza del percorso del file?)
  • il percorso completo del file, un byte per carattere
  • 12 byte di origine sconosciuta, come pubblicato da James (40 25 01 00 78 9c 00 2a 40 d5 bf).

Per eliminare i byte di intestazione, ho fatto questo:

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

Da lì, inizia il file PNG attuale. È facile verificarlo dalla sequenza di avvio a 8 byte PNG.

Ho proceduto a cercare di capire dove si trovavano i byte extra leggendo le intestazioni PNG e confrontando la lunghezza passata nel blocco IDAT con la lunghezza dei dati implicita dedotta dalla misurazione del numero di byte fino al blocco IEND. (per i dettagli, controlla il file bsa.py su github)

Le dimensioni fornite dai blocchi in n_letter.png sono:

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

Quando ho misurato la distanza effettiva tra il blocco IDAT e il blocco IEND dopo di esso (contando i byte usando string.find () in Python), ho scoperto che la lunghezza dell'IDAT implicita era di 60640 byte - c'erano altri 15 byte .

In generale, nella maggior parte dei file "lettera" erano presenti 5 byte extra per ogni 16 KB di dimensioni totali del file. Ad esempio, o_letter.png, a circa 73 KB, aveva 20 byte extra. I file più grandi, come gli arcani scarabocchi, seguivano per lo più lo stesso modello, sebbene alcuni avessero aggiunto quantità dispari su (52 byte, 12 byte o 32 byte). Non ho idea di cosa stia succedendo lì.

Per il file n_letter.png, sono stato in grado di trovare gli offset corretti (principalmente per tentativi ed errori) in cui rimuovere i segmenti a 5 byte.

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

I segmenti di cinque byte rimossi sono:

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

Per quello che vale, ho incluso gli ultimi cinque byte del segmento sconosciuto a 12 byte a causa di una certa somiglianza con le altre sequenze.

Si scopre che non sono abbastanza ogni 16 KB, ma a intervalli di ~ 0x4030 byte.

Per evitare di ottenere corrispondenze ravvicinate ma non perfette negli indici sopra, ho anche testato la decompressione zlib del blocco IDAT dal PNG risultante e passa.


"1 byte per un segno casuale @" è la lunghezza della stringa del nome file, credo
James Tauber,

qual è il valore dei segmenti a 5 byte in ciascun caso?
James Tauber,

Ho aggiornato la mia risposta con valori esadecimali dei segmenti a 5 byte rimossi. Inoltre, mi ero confuso con il numero di segmenti a 5 byte (in precedenza stavo contando la misteriosa intestazione a 12 byte come intestazione da 7 byte e 5 byte ripetendo il divisore). Ho risolto anche quello.
tillberg,

si noti che (little-endian) 0x402A, 0x4030, 0x402B appaiono in quei segmenti di 5 byte; sono gli intervalli effettivi?
James Tauber,

Pensavo di aver già detto che era un lavoro eccellente, ma a quanto pare non l'ho fatto. Lavoro eccellente! :-)
sam hocevar,

3

In realtà, i 5 byte intermittenti fanno parte della compressione zlib.

Come dettagliato su http://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/ ,

01 la piccola stringa di bit endian 1 00 00000. 1 che indica il blocco finale, 00 che indica un blocco non compresso e 00000 sono 5 bit di riempimento per allineare l'inizio di un blocco su un ottetto (necessario per i blocchi non compressi e molto conveniente per me). 05 00 fa ff Il numero di ottetti di dati nel blocco non compresso (5). Memorizzato come intero a 16 bit little-endian seguito dal suo complemento a 1 (!).

.. quindi un 00 indica un blocco 'successivo' (non uno finale), e i 4 byte successivi sono la lunghezza del blocco e il suo inverso.

[Modifica] Una fonte più affidabile è ovviamente RFC 1951 (Deflate Compressed Data Format Specification), sezione 3.2.4.


1

È possibile che tu stia leggendo i dati dal file in una modalità di testo (dove i finali di linea che appaiono nei dati PNG sono probabilmente alterati) invece che in una modalità binaria?


1
Sì. Sembra molto simile al problema. Considerando questo è il codice che lo legge: github.com/jtauber/skyrim/blob/master/bsa.py --- confermato :-)
Armin Ronacher il

No, non fa differenza.
James Tauber,

@JamesTauber, se stai davvero codificando il tuo caricatore PNG come suggerisce il commento di Armin, allora (a) funziona su altri PNG che hai provato e (b) fa un caricatore PNG collaudato come libpngleggere i PNG Skyrim? In altre parole, è solo un bug nel tuo caricatore PNG?
Nathan Reed,

@NathanReed Tutto quello che sto facendo è estrarre il flusso di byte e caricarlo qui; non vi è alcun "caricatore" coinvolto
James Tauber,

3
-1, questo non può essere il motivo. Se i file PNG fossero danneggiati in questo modo, ci sarebbero errori CRC nella fase di gonfiaggio molto prima degli errori nella fase di decodifica dell'immagine. Inoltre, non ci sono occorrenze di CRLF nei file oltre a quello previsto nell'intestazione.
Sam Hocevar,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.