Pourquoi certains fichiers PNG extraits des jeux ne s'affichent pas correctement?

14

J'ai remarqué l'extraction de fichiers PNG à partir de certains fichiers de jeu que l'image se déforme en cours de route. Par exemple, voici quelques PNG extraits du fichier Textures dans Skyrim:

J PNG illuminé de Skyrim Illuminé K PNG de Skyrim

Est-ce une variation inhabituelle sur un format PNG? Quelles modifications devrais-je apporter pour afficher correctement ces fichiers PNG?

James Tauber
la source
1
Peut-être qu'ils ont mis un encodage spécial dans leurs fichiers pour empêcher les gens de faire des choses comme ça. Ou peut-être que tout ce que vous utilisez pour extraire ne fonctionne pas correctement.
Richard Marskell - Drackir
C'est peut-être une sorte de compression pour rendre les images plus petites en taille de fichier. Cela se fait également dans les applications iPhone.
replié à droite
1
Un peu hors sujet, mais est-ce un poney?
jcora

Réponses:

22

Voici les images «restaurées», grâce aux recherches approfondies de tillberg:

final1 final2

Comme prévu, il y a un marqueur de bloc de 5 octets tous les 0x4020 octets environ. Le format semble être le suivant:

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

Une fois le marqueur lu, les marker.lenoctets suivants forment un bloc qui fait partie du fichier. marker.notlenest une variable de contrôle telle que marker.len + marker.notlen == 0xffff. Le dernier bloc est tel que marker.tag == 1.

La structure est probablement la suivante. Il existe encore des valeurs inconnues.

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

Je n'ai pas compris ce qui est à la fin, mais comme les PNG acceptent le rembourrage, ce n'est pas trop dramatique. Cependant, la taille du fichier encodé indique clairement que les 4 derniers octets doivent être ignorés ...

Comme je n'avais pas accès à tous les marqueurs de bloc juste avant le début du fichier, j'ai écrit ce décodeur qui commence à la fin et tente de trouver les marqueurs de bloc. Ce n'est pas du tout robuste mais bon, cela a fonctionné pour vos images de test:

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

Recherches plus anciennes

Voici ce que vous obtenez en supprimant l'octet 0x4022de la deuxième image, puis en supprimant l'octet 0x8092:

original premier pas deuxième étape

Il ne «répare» pas vraiment les images; Je l'ai fait par essais et erreurs. Cependant, il indique qu'il y a des données inattendues tous les 16384 octets. Je suppose que les images sont regroupées dans une sorte de structure de système de fichiers et que les données inattendues sont simplement des marqueurs de bloc que vous devez supprimer lors de la lecture des données.

Je ne sais pas exactement où sont les marqueurs de bloc et leur taille, mais la taille du bloc elle-même est très certainement de 2 ^ 14 octets.

Il serait utile que vous puissiez également fournir un vidage hexadécimal (quelques dizaines d'octets) de ce qui apparaît juste avant l'image et juste après. Cela donnerait des indications sur le type d'informations stockées au début ou à la fin des blocs.

Bien sûr, il y a aussi la possibilité qu'il y ait un bug dans votre code d'extraction. Si vous utilisez un tampon de 16384 octets pour vos opérations sur les fichiers, je vérifierais d'abord.

sam hocevar
la source
+1 très utile; Je vais continuer à creuser avec le plomb que vous m'avez donné et publier des informations supplémentaires
James Tauber
Le "fichier" incorporé commence par une chaîne préfixée par la longueur contenant le nom du fichier; suivi de 12 octets avant la magie 89 50 4e 47 pour les fichiers PNG. Les 12 octets sont: 40 25 01 00 78 9c 00 2a 40 d5 bf
James Tauber
Beau travail, Sam. J'ai mis à jour le code python qui lit réellement les fichiers BSA directement pour faire de même. Les résultats sont visibles sur orbza.s3.amazonaws.com/tillberg/pics.html (je n'y montre que 1/3 des images, juste assez pour démontrer les résultats). Cela fonctionne pour de nombreuses images. Il y a d'autres choses qui se passent avec certaines des autres images. Je me demande si cela a été résolu ailleurs dans Fallout 3 ou Skyrim, cependant.
tillberg
Excellent travail, les gars! Je mettrai aussi mon code à jour
James Tauber
18

Sur la base de la suggestion de Sam, j'ai bifurqué le code de James sur https://github.com/tillberg/skyrim et j'ai réussi à extraire n_letter.png du fichier Skyrim Textures BSA.

La lettre N

La "taille_fichier" donnée par les en-têtes BSA n'est pas la taille réelle du fichier final. Il comprend des informations d'en-tête ainsi que des morceaux aléatoires de données apparemment inutiles dispersées.

Les en-têtes ressemblent à ceci:

  • 1 octet (longueur du chemin du fichier?)
  • le chemin complet du fichier, un octet par caractère
  • 12 octets d'origine inconnue, comme James l'a signalé (40 25 01 00 78 9c 00 2a 40 d5 bf).

Pour supprimer les octets d'en-tête, j'ai fait ceci:

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

De là, le fichier PNG réel commence. Il est facile de vérifier cela à partir de la séquence de démarrage PNG de 8 octets.

J'ai essayé de comprendre où se trouvaient les octets supplémentaires en lisant les en-têtes PNG et en comparant la longueur passée dans le bloc IDAT à la longueur de données implicite déduite de la mesure du nombre d'octets jusqu'au bloc IEND. (pour plus de détails, consultez le fichier bsa.py sur github)

Les tailles données par les morceaux dans n_letter.png sont:

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

Lorsque j'ai mesuré la distance réelle entre le bloc IDAT et le bloc IEND après celui-ci (en comptant les octets à l'aide de string.find () en Python), j'ai constaté que la longueur IDAT réelle impliquée était de 60640 octets - il y avait 15 octets supplémentaires .

En général, la plupart des fichiers "lettre" avaient 5 octets supplémentaires présents pour chaque 16 Ko de taille totale de fichier. Par exemple, o_letter.png, à environ 73 Ko, avait 20 octets supplémentaires. Les fichiers plus volumineux, comme les gribouillis obscurs, suivaient généralement le même schéma, bien que certains aient des quantités étranges ajoutées (52 octets, 12 octets ou 32 octets). Aucune idée de ce qui se passe là-bas.

Pour le fichier n_letter.png, j'ai pu trouver les décalages corrects (principalement par essais et erreurs) pour supprimer les segments de 5 octets.

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

Les cinq segments d'octets supprimés sont:

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

Pour ce que ça vaut, j'ai inclus les cinq derniers octets du segment inconnu de 12 octets en raison de certaines similitudes avec les autres séquences.

Il s'avère qu'ils ne sont pas tout à fait tous les 16 Ko, mais à des intervalles de ~ 0x4030 octets.

Pour éviter d'obtenir des correspondances proches mais pas parfaites dans les indices ci-dessus, j'ai également testé la décompression zlib du bloc IDAT à partir du PNG résultant, et cela passe.

tillberg
la source
le "1 octet pour un signe @ aléatoire" est la longueur de la chaîne de nom de fichier, je crois
James Tauber
quelle est la valeur des segments de 5 octets dans chaque cas?
James Tauber
J'ai mis à jour ma réponse avec les valeurs hexadécimales des segments de 5 octets supprimés. De plus, je m'étais mélangé sur le nombre de segments de 5 octets (je comptais auparavant l'en-tête mystérieux de 12 octets comme un en-tête de 7 octets et un diviseur répétitif de 5 octets). J'ai corrigé cela aussi.
tillberg
notez que (little-endian) 0x402A, 0x4030, 0x402B apparaissent dans ces segments de 5 octets; sont-ils les intervalles réels?
James Tauber
Je pensais avoir déjà dit que c'était un excellent travail, mais apparemment je ne l'ai pas fait. Excellent travail! :-)
sam hocevar
3

En fait, les 5 octets intermittents font partie de la compression zlib.

Comme détaillé sur http://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/ ,

01 la petite chaîne de bits endian 1 00 00000. 1 indiquant le bloc final, 00 indiquant un bloc non compressé et 00000 sont 5 bits de remplissage pour aligner le début d'un bloc sur l'octet (qui est requis pour les blocs non compressés , et très pratique pour moi). 05 00 fa ff Le nombre d'octets de données dans le bloc non compressé (5). Stocké sous la forme d'un entier 16 bits peu endien suivi de son complément 1 (!).

.. donc un 00 indique un bloc 'suivant' (pas un bloc de fin), et les 4 octets suivants sont la longueur du bloc et son inverse.

[Modifier] Une source plus fiable est bien sûr la RFC 1951 (Deflate Compressed Data Format Specification), section 3.2.4.

jongware
la source
1

Est-il possible que vous lisiez les données du fichier en mode texte (où les fins de ligne qui apparaissent dans les données PNG sont éventuellement altérées) plutôt qu'en mode binaire?

Greg Hewgill
la source
1
Toujours. Cela ressemble beaucoup au problème. Considérant que c'est le code qui le lit: github.com/jtauber/skyrim/blob/master/bsa.py --- confirmé :-)
Armin Ronacher
Non, cela ne fait aucune différence.
James Tauber
@JamesTauber, si vous codez vraiment votre propre chargeur PNG comme le suggère le commentaire d'Armin, alors (a) cela fonctionne-t-il sur les autres PNG que vous avez essayés, et (b) un chargeur PNG éprouvé tel que libpnglire les PNG Skyrim? En d'autres termes, s'agit-il simplement d'un bug dans votre chargeur PNG?
Nathan Reed
@NathanReed tout ce que je fais est d'extraire le flux d'octets et de le télécharger ici; il n'y a pas de "chargeur" ​​impliqué
James Tauber
3
-1, cela ne peut pas être la raison. Si les fichiers PNG étaient corrompus de cette manière, il y aurait des erreurs CRC au stade du gonflage bien avant les erreurs au stade du décodage de l'image. En outre, il n'y a aucune occurrence de CRLF dans les fichiers à part celle attendue dans l'en-tête.
sam hocevar