¿Por qué algunos archivos PNG extraídos de los juegos se muestran incorrectamente?

14

He notado que al extraer PNG de algunos archivos del juego, la imagen se distorsiona a la mitad. Por ejemplo, aquí hay un par de PNG extraídos del archivo de Texturas en Skyrim:

Iluminado J PNG de Skyrim Iluminado K PNG de Skyrim

¿Es esta una variación inusual en un formato PNG? ¿Qué modificaciones necesitaría hacer para ver dichos PNG correctamente?

James Tauber
fuente
1
Quizás hayan puesto una codificación especial en sus archivos para evitar que las personas hagan cosas como esta. O tal vez lo que sea que esté usando para extraer no funcione correctamente.
Richard Marskell - Drackir
Tal vez sea una especie de compresión hacer que las imágenes sean más pequeñas en tamaño de archivo. Esto también se hace en aplicaciones de iPhone.
derecha el
1
Un poco fuera de tema, pero ¿es eso un pony?
jcora

Respuestas:

22

Aquí están las imágenes "restauradas", gracias a la investigación adicional de tillberg:

final1 final2

Como se esperaba, hay un marcador de bloque de 5 bytes cada 0x4020 bytes. El formato parece ser el siguiente:

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

Una vez que se ha leído el marcador, los siguientes marker.lenbytes forman un bloque que forma parte del archivo. marker.notlenes una variable de control de tal manera que marker.len + marker.notlen == 0xffff. El último bloque es tal que marker.tag == 1.

La estructura es probablemente la siguiente. Todavía hay valores desconocidos.

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

No he descubierto qué hay al final, pero como los PNG aceptan el relleno, no es demasiado dramático. Sin embargo, el tamaño del archivo codificado indica claramente que los últimos 4 bytes deben ignorarse ...

Como no tenía acceso a todos los marcadores de bloque justo antes del comienzo del archivo, escribí este decodificador que comienza al final e intenta encontrar los marcadores de bloque. No es robusto en absoluto, pero bueno, funcionó para sus imágenes de prueba:

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

Investigación más antigua

Esto es lo que obtienes al eliminar el byte 0x4022de la segunda imagen, luego al eliminar el byte 0x8092:

original primer paso segundo paso

Realmente no "repara" las imágenes; Lo hice por prueba y error. Sin embargo, lo que dice es que hay datos inesperados cada 16384 bytes. Supongo que las imágenes están empaquetadas en algún tipo de estructura de sistema de archivos y los datos inesperados son simplemente marcadores de bloque que debe eliminar al leer los datos.

No sé exactamente dónde están los marcadores de bloque y su tamaño, pero el tamaño del bloque en sí es ciertamente 2 ^ 14 bytes.

Sería útil si también pudiera proporcionar un volcado hexadecimal (unas pocas docenas de bytes) de lo que aparece justo antes de la imagen y justo después. Esto daría pistas sobre qué tipo de información se almacena al principio o al final de los bloques.

Por supuesto, también existe la posibilidad de que haya un error en su código de extracción. Si está utilizando un búfer de 16384 bytes para sus operaciones de archivo, entonces primero comprobaría allí.

sam hocevar
fuente
+1 muy útil; Continuaré profundizando en esto con el liderazgo que me has dado y publicaré información adicional
James Tauber
El "archivo" incrustado comienza con una cadena de longitud prefijada que contiene el nombre del archivo; seguido de 12 bytes antes de la magia 89 50 4e 47 para archivos PNG. Los 12 bytes son: 40 25 01 00 78 9c 00 2a 40 d5 bf
James Tauber
Buen trabajo, Sam. Actualicé el código de Python que realmente lee los archivos BSA directamente para hacer lo mismo. Los resultados son visibles en orbza.s3.amazonaws.com/tillberg/pics.html (solo estoy mostrando 1/3 de las imágenes, lo suficiente para demostrar los resultados). Esto funciona para muchas de las imágenes. Hay algunas otras cosas sucediendo con algunas de las otras imágenes. Sin embargo, me pregunto si esto se ha resuelto en otro lugar, como Fallout 3 o Skyrim.
hasta el
Excelente trabajo, muchachos! También actualizaré mi código
James Tauber
18

Basado en la sugerencia de Sam, bifurqué el código de James en https://github.com/tillberg/skyrim y pude extraer con éxito n_letter.png del archivo BSA de Skyrim Textures.

La letra n

El "tamaño_archivo" dado por los encabezados BSA no es el tamaño real del archivo final. Incluye información de encabezado, así como algunos fragmentos aleatorios de datos aparentemente inútiles dispersos.

Los encabezados se parecen a esto:

  • 1 byte (longitud de la ruta del archivo?)
  • La ruta completa del archivo, un byte por carácter
  • 12 bytes de origen desconocido, como James publicó (40 25 01 00 78 9c 00 2a 40 d5 bf).

Para quitar los bytes del encabezado, hice esto:

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

A partir de ahí, comienza el archivo PNG real. Es fácil verificar eso desde la secuencia de inicio de 8 bytes de PNG.

Procedí a tratar de averiguar dónde estaban ubicados los bytes adicionales leyendo los encabezados PNG y comparando la longitud pasada en el fragmento IDAT con la longitud de datos implícita deducida de la medición del número de bytes hasta el fragmento IEND. (para obtener más información al respecto, consulte el archivo bsa.py en github)

Los tamaños dados por los fragmentos en n_letter.png son:

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

Cuando medí la distancia real entre el fragmento IDAT y el fragmento IEND después (contando bytes usando string.find () en Python), descubrí que la longitud real de IDAT implicada era 60640 bytes, había 15 bytes adicionales allí .

En general, la mayoría de los archivos "carta" tenían 5 bytes adicionales presentes por cada 16 KB de tamaño total de archivo. Por ejemplo, o_letter.png, con alrededor de 73 KB, tenía 20 bytes adicionales. Los archivos más grandes, como los garabatos arcanos, en su mayoría seguían el mismo patrón, aunque algunos tenían cantidades extra (52 bytes, 12 bytes o 32 bytes). No tengo idea de lo que está pasando allí.

Para el archivo n_letter.png, pude encontrar las compensaciones correctas (principalmente por prueba y error) en las que eliminar los segmentos de 5 bytes.

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

Los cinco segmentos de byte eliminados son:

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

Para lo que vale, he incluido los últimos cinco bytes del segmento desconocido de 12 bytes debido a alguna similitud con las otras secuencias.

Resulta que no son exactamente cada 16 KB, pero a intervalos de ~ 0x4030 bytes.

Para evitar contraer coincidencias cercanas pero no perfectas en los índices anteriores, también probé la descompresión zlib del fragmento IDAT del PNG resultante, y pasa.

tillberg
fuente
el "1 byte para un signo @ aleatorio" es la longitud de la cadena del nombre de archivo, creo
James Tauber
¿Cuál es el valor de los segmentos de 5 bytes en cada caso?
James Tauber
Actualicé mi respuesta con valores hexadecimales de los segmentos de 5 bytes eliminados. Además, me había mezclado sobre la cantidad de segmentos de 5 bytes (anteriormente contaba el misterioso encabezado de 12 bytes como un encabezado de 7 bytes y un divisor repetitivo de 5 bytes). Yo también lo arreglé.
hasta el
tenga en cuenta que (little-endian) 0x402A, 0x4030, 0x402B aparecen en esos segmentos de 5 bytes; ¿Son los intervalos reales?
James Tauber
Pensé que ya había dicho que este era un trabajo excelente, pero aparentemente no lo hice. ¡Excelente trabajo! :-)
sam hocevar
3

En realidad, los 5 bytes intermitentes son parte de la compresión zlib.

Como se detalla en http://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/ ,

01 la cadena de bits little endian 1 00 00000. 1 que indica el bloque final, 00 que indica un bloque no comprimido y 00000 son 5 bits de relleno para alinear el inicio de un bloque con el octeto (que se requiere para bloques no comprimidos) y muy conveniente para mi). 05 00 fa ff El número de octetos de datos en el bloque sin comprimir (5). Almacenado como un entero de 16 bits little endian seguido de su complemento de 1 (!).

.. entonces un 00 indica un bloque 'siguiente' (no uno final), y los 4 bytes siguientes son la longitud del bloque y su inverso.

[Editar] Una fuente más confiable es, por supuesto, RFC 1951 (Deflate Compressed Data Format Specification), sección 3.2.4.

jongware
fuente
1

¿Es posible que esté leyendo los datos del archivo en un modo de texto (donde las terminaciones de línea que aparecen en los datos PNG posiblemente estén destrozadas) en lugar de en un modo binario?

Greg Hewgill
fuente
1
Sí. Eso se parece mucho al problema. Teniendo en cuenta que este es el código que lo lee: github.com/jtauber/skyrim/blob/master/bsa.py --- confirmado :-)
Armin Ronacher
No, no hay diferencia.
James Tauber
@JamesTauber, si realmente está codificando su propio cargador PNG como parece implicar el comentario de Armin, entonces (a) ¿funciona en otros PNG que ha probado y (b) un cargador PNG probado como libpngleer los PNG de Skyrim? En otras palabras, ¿es solo un error en su cargador PNG?
Nathan Reed
@NathanReed todo lo que estoy haciendo es extraer el flujo de bytes y subirlo aquí; no hay ningún "cargador" involucrado
James Tauber
3
-1, esta no puede ser la razón. Si los archivos PNG se corrompieran de esta manera, habría errores de CRC en la etapa de inflado mucho antes de los errores en la etapa de decodificación de imágenes. Además, no hay ocurrencias de CRLF en los archivos, aparte de la esperada en el encabezado.
sam hocevar