Por que alguns arquivos PNG extraídos dos jogos são exibidos incorretamente?


14

Eu notei que extrair PNGs de alguns arquivos de jogo nos quais a imagem fica distorcida parcialmente. Por exemplo, aqui estão alguns PNGs extraídos do arquivo Textures no Skyrim:

J PNG iluminado de Skyrim K iluminado PNG de Skyrim

É alguma variação incomum em um formato PNG? Que modificações eu precisaria fazer para visualizar adequadamente esses PNGs?


1
Talvez eles tenham inserido uma codificação especial em seus arquivos para impedir que as pessoas façam coisas assim. Ou talvez o que você está usando para extrair não funcione corretamente.
Richard Marskell - Drackir

Talvez seja um tipo de compactação para diminuir o tamanho das imagens. Isso também é feito em aplicativos para iPhone.
rightfold

1
Um pouco fora de tópico, mas isso é um pônei?
Jcora #

Respostas:


22

Aqui estão as imagens "restauradas", graças às pesquisas adicionais de tillberg:

final1 final2

Como esperado, há um marcador de bloco de 5 bytes a cada 0x4020 bytes. O formato parece ser o seguinte:

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

Após a leitura do marcador, os próximos marker.lenbytes formam um bloco que faz parte do arquivo. marker.notlené uma variável de controle tal que marker.len + marker.notlen == 0xffff. O último bloco é tal que marker.tag == 1.

A estrutura é provavelmente a seguinte. Ainda existem valores desconhecidos.

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

Ainda não descobri o que há no final, mas como os PNGs aceitam o preenchimento, não é muito dramático. No entanto, o tamanho do arquivo codificado indica claramente que os últimos 4 bytes devem ser ignorados ...

Como não tinha acesso a todos os marcadores de bloco imediatamente antes do início do arquivo, escrevi esse decodificador que inicia no final e tenta encontrar os marcadores de bloco. Não é robusto, mas funciona bem para as imagens de teste:

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

Pesquisa mais antiga

É isso que você obtém ao remover o byte 0x4022da segunda imagem e, em seguida, ao remover o byte 0x8092:

original Primeiro passo segundo passo

Realmente não "repara" as imagens; Eu fiz isso por tentativa e erro. No entanto, o que diz é que há dados inesperados a cada 16384 bytes. Meu palpite é que as imagens são compactadas em algum tipo de estrutura do sistema de arquivos e os dados inesperados são simplesmente marcadores de blocos que você deve remover ao ler os dados.

Não sei exatamente onde estão os marcadores de bloco e seu tamanho, mas o tamanho do bloco em si é certamente 2 ^ 14 bytes.

Ajudaria se você também pudesse fornecer um dump hexadecimal (algumas dezenas de bytes) do que aparece logo antes da imagem e logo depois. Isso daria dicas sobre que tipo de informação é armazenada no início ou no final dos blocos.

Claro que também existe a possibilidade de que haja um erro no seu código de extração. Se você estiver usando um buffer de 16384 bytes para suas operações de arquivo, eu primeiro verificaria lá.


+1 muito útil; Vou continuar a investigar isso com a liderança que você me deu e postar mais informações #
James Tauber

O "arquivo" incorporado começa com uma sequência prefixada de comprimento contendo o nome do arquivo; seguido por 12 bytes antes da mágica 89 50 4e 47 para arquivos PNG. Os 12 bytes são: 40 25 01 00 78 9c 00 2a 40 d5 bf
James Tauber

Bom trabalho, Sam. Atualizei o código python que realmente lê os arquivos BSA diretamente para fazer o mesmo. Os resultados são visíveis em orbza.s3.amazonaws.com/tillberg/pics.html (estou mostrando apenas 1/3 das imagens, apenas o suficiente para demonstrar os resultados). Isso funciona para muitas das imagens. Há outras coisas acontecendo com algumas das outras imagens. Eu estou querendo saber se isso foi resolvido em outro lugar re Fallout 3 ou Skyrim, no entanto.
Tillberg

Excelente trabalho, pessoal! Eu vou atualizar meu código também
James Tauber

18

Com base na sugestão de Sam, peguei o código de James em https://github.com/tillberg/skyrim e consegui extrair com sucesso o n_letter.png do arquivo BSA do Skyrim Textures.

A letra N

O "tamanho_do_arquivo" fornecido pelos cabeçalhos da BSA não é o tamanho final real do arquivo. Ele inclui algumas informações de cabeçalho, bem como alguns blocos aleatórios de dados aparentemente inúteis espalhados.

Os cabeçalhos são mais ou menos assim:

  • 1 byte (comprimento do caminho do arquivo?)
  • o caminho completo do arquivo, um byte por caractere
  • 12 bytes de origem desconhecida, conforme publicado por James (40 25 01 00 78 9c 00 2a 40 d5 bf).

Para retirar os bytes do cabeçalho, fiz o seguinte:

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

A partir daí, o arquivo PNG real é iniciado. É fácil verificar isso na sequência de início de 8 bytes do PNG.

Eu tentei descobrir onde os bytes extras estavam localizados lendo os cabeçalhos PNG e comparando o comprimento passado no pedaço IDAT com o comprimento implícito dos dados deduzido da medição do número de bytes até o pedaço IEND. (para detalhes, confira o arquivo bsa.py no github)

Os tamanhos fornecidos pelos blocos em n_letter.png são:

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

Quando medi a distância real entre o pedaço IDAT e o pedaço IEND depois dele (contando bytes usando string.find () em Python), descobri que o comprimento real do IDAT implícito era 60640 bytes - havia 15 bytes extras lá .

Em geral, a maioria dos arquivos "letter" tinha 5 bytes extras presentes para cada 16 KB do tamanho total do arquivo. Por exemplo, o_letter.png, com cerca de 73 KB, tinha mais 20 bytes. Arquivos maiores, como os rabiscos arcanos, geralmente seguiam o mesmo padrão, embora alguns tivessem valores ímpares adicionados (52 bytes, 12 bytes ou 32 bytes). Não faço ideia do que está acontecendo lá.

Para o arquivo n_letter.png, consegui encontrar as compensações corretas (principalmente por tentativa e erro) nas quais remover os 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)

Os segmentos de cinco bytes removidos são:

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

Quanto vale a pena, incluí os últimos cinco bytes do segmento desconhecido de 12 bytes por causa de alguma semelhança com as outras seqüências.

Acontece que eles não são bem a cada 16 KB, mas a intervalos de ~ 0x4030 bytes.

Para me impedir de obter correspondências quase perfeitas nos índices acima, também testei a descompressão zlib do pedaço IDAT do PNG resultante e ele passa.


o "1 byte para um aleatório sinal @" é o comprimento da corda filename, acredito
James Tauber

qual é o valor dos segmentos de 5 bytes em cada caso?
James Tauber

Atualizei minha resposta com valores hexadecimais dos segmentos de 5 bytes removidos. Além disso, eu me confundi com o número de segmentos de 5 bytes (eu estava contando o misterioso cabeçalho de 12 bytes como cabeçalho de 7 bytes e divisor de repetição de 5 bytes). Eu consertei isso também.
tillberg

observe que (little-endian) 0x402A, 0x4030, 0x402B aparecem nesses segmentos de 5 bytes; eles são os intervalos reais?
James Tauber

Eu pensei que já tinha dito que era um excelente trabalho, mas aparentemente não o fiz. Excelente trabalho! :-)
sam hocevar

3

Na verdade, os 5 bytes intermitentes fazem parte da compactação zlib.

Conforme detalhado em http://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/ ,

01 a pequena sequência de bits endian 1 00 00000. 1 indicando o bloco final, 00 indicando um bloco não compactado e 00000 são 5 bits de preenchimento para alinhar o início de um bloco no octeto (o que é necessário para blocos não compactados) e muito conveniente para mim). 05 00 fa ff O número de octetos de dados no bloco não compactado (5). Armazenado como um número inteiro de 16 bits little endian seguido pelo seu complemento de 1 (!).

.. então um 00 indica um bloco 'próximo' (não um final) e os 4 bytes seguintes são o comprimento do bloco e seu inverso.

[Editar] Uma fonte mais confiável é, obviamente, a RFC 1951 (Deflate Compressed Data Format Specification), seção 3.2.4.


1

É possível que você esteja lendo os dados do arquivo no modo de texto (onde as terminações de linha que aparecem nos dados PNG são possivelmente mutiladas) em vez de no modo binário?


1
Sim. Isso parece muito com o problema. Considerando este é o código que lê-lo: github.com/jtauber/skyrim/blob/master/bsa.py --- confirmou :-)
Armin Ronacher

Não, não faz diferença.
James Tauber

@ JamesTauber, se você realmente está codificando seu próprio carregador PNG, como o comentário de Armin parece sugerir, então (a) funciona em outros PNGs que você já tentou e (b) um carregador PNG comprovado, como libpnglê os PNG Skyrim? Em outras palavras, é apenas um bug no seu carregador PNG?
Nathan Reed

@NathanReed, tudo o que estou fazendo é extrair o fluxo de bytes e enviá-lo aqui; não há "loader" envolvido
James Tauber

3
-1, esse não pode ser o motivo. Se os arquivos PNG fossem corrompidos dessa maneira, haveria erros de CRC no estágio de inflamento muito antes dos erros no estágio de decodificação da imagem. Além disso, não há ocorrências de CRLF nos arquivos além da esperada no cabeçalho.
sam hocevar
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.