Formatinhos: BMP - Parte 3: Layout do pixel
Na primeira parte, aprendemos o básico sobre como ler um arquivo de bitmap.
Na segunda parte, aprendemos a ler bitmaps com diferentes densidades de pixel.
Agora, iremos ver diferentes densidades de pixel.
Densidades de pixel⌗
Por padrão, o arquivo de bitmap possui uma densidade e organização padrão para o pixel.
As funções getColors da parte anterior mostram isso.
Porém, alguns programas podem mudar essa ordem, por exemplo, para otimizar certas cores. O arquivo
que tem a “compressão” BITFIELDS possui essa capacidade.
Essa ordem é especificada em um tipo de valor chamado de ‘bitmask’.
Bitmask⌗
Vem do inglês e significa, literalmente, máscara de bits. É basicamente um jeito de “cortar” um valor: bits 1 são mantidos e bits 0 sao cortados. Por exemplo:
Valor: 42069
Máscara: 496
Convertemos esses valores para binário
Valor__: 1010 0100 0101 0101Máscara: 0000 0001 1111 0000
Fazemos uma operação AND. Isso significa que, se os dois valores forem 1, o resultado será 1,
do contrário o resultado será 0.
Valor resultante: 0000 0000 0101 0000
Convertemos de volta para decimal:
Valor resultante: 80
Além disso, iremos fazer mais uma operação, a operação de shifting. Contamos quantos bits temos até o primeiro valor 1 e shiftamos esse valor para a direita.
No caso, o valor é 4.
Valor final: 0000 0000 0101 0000 » 4 = 0000 0000 0000 0101
Valor final: 5
Leitura⌗
Agora que você já sabe como a operação de bitmask funciona, iremos ler esse valor.
Logo depois do header DIB (lembre-se da parte 1), se a compressão for BITFIELDS, temos quatro
campos a mais:
00000020  00 00 00 00 10 00  d7 0d  00 00 d7 0d 00 00 [00 00  |................|
00000030  00 00 00 00 00 00  00 00  ff 00 00 ff 00 00  ff 00  |................|
00000040  00 00 00 00 00 ff] 00 00  00 00 00 00 00 00  00 00  |................|
00000050  00 00 00 00 00 00  00 00  00 00 00 00 00 00  00 00  |................|
| Posição | Tamanho | Campo | Valor | 
|---|---|---|---|
0x20 (32) | 4 | biClrUsed | 0 | 
0x24 (36) | 4 | biClrImportant | 0 | 
| 0x28 (40) | 4 | bV5RedMask | 00 00 ff 00 (0x00ff_0000) | 
| 0x2c (44) | 4 | bV5GreenMask | 00 ff 00 00 (0x0000_ff00) | 
| 0x30 (48) | 4 | bV5BlueMask | ff 00 00 00 (0x0000_00ff) | 
| 0x34 (52) | 4 | bV5AlphaMask | 00 00 00 ff (0xff00_0000) | 
O tamanho desses valores é sempre 32 bits, mas ele é usado para extrair valores em densidades de bits menores. Você simplesmente ignora os outros bits.
O red mask é usado para extrair o vermelho, o green é usado pra extrair o verde, o blue é
usado para extrair o azul e o alpha é usado para extrair o canal de transparência.
Por padrão, cada pixel é organizado desse jeito:
16 bits:
15 14 13 12 11 10 09 08 | 07 06 05 04 03 02 01 00
-  R  R  R  R  R  G  G  | G  G  G  B  B  B  B  B
24 bits:
------------------------| 23 22 21 20 19 18 17 16
-  -  -  -  -  -  -  -  | R  R  R  R  R  R  R  R
15 14 13 12 11 10 09 08 | 07 06 05 04 03 02 01 00
G  G  G  G  G  G  G  G  | B  B  B  B  B  B  B  B
32 bits:
31 30 29 28 27 26 25 24 | 23 22 21 20 19 18 17 16
-  -  -  -  -  -  -  -  | R  R  R  R  R  R  R  R
15 14 13 12 11 10 09 08 | 07 06 05 04 03 02 01 00
G  G  G  G  G  G  G  G  | B  B  B  B  B  B  B  B
Traduzir isso para bitmasks será um exercício para o leitor.
Em código, a leitura e interpretação das bitmasks é a seguinte:
function openDIBHeader(buffer) {
    /* ... */
    const dibHeader = {
        compression: parseCompression(get32(buffer, 30)),
        bitmapSize: get32(buffer, 34),
        horizontalRes: get32(buffer, 38),
        verticalRes: get32(buffer, 42),
        paletteColors: get32(buffer, 46),
        importantColors: get32(buffer, 50)
    };
    /* adicione essa linha aqui: */
    const headerBitfields = {
        red: get32(buffer, 54),
        green: get32(buffer, 58),
        blue: get32(buffer, 62),
        alpha: get32(buffer, 66),
    }
    /* é esse código acima que você tem que botar */
    return {
        size: headerSize,
        ...baseHeader,
        ...(headerSize >= 40 ? dibHeader : {}),
        // isso aqui embaixo também: 
        bitfields: (dibHeader.compression === 'bitfields' ? headerBitfields : null)
    }
}
/* Pega a bitmask e te diz quanto você precisa shiftar e em qual valor
 * você precisa dar AND para ter o valor de cor baseado na bitmask
 *
 * Não é o jeito mais otimizado de fazer isso, mas é um jeito correto.
 */
function bitmaskIntoShiftAndMask(bitmask) {
    let shift = 0;
    let mask = bitmask;
    while (!(mask & 0x1)) {
        if (mask === 0)
            break;
        mask = mask >>> 1;
        shift++;
    }
    return { shift, mask };
}
/* Pega a bitmask dos quatro canais */
function getBitmasks(bitfields) {
    if (!bitfields)
        return null;
    return {
        r: bitmaskIntoShiftAndMask(bitfields.red),
        g: bitmaskIntoShiftAndMask(bitfields.green),
        b: bitmaskIntoShiftAndMask(bitfields.blue),
        a: bitmaskIntoShiftAndMask(bitfields.alpha),
    }
}
/* Meio auto-explicativo essa :P */
function getValueFromBitmask(value, { shift, mask }) {
    return (value >>> shift) & mask;
}
/**
 * Vou deixar como exemplo uma das densidades de cor aqui, para você ver como 
 * extrair a cor do pixel baseados nas bitmasks.
 *
 * O parâmetro bitmask tem como formato o resultado da função getBitmasks()
 *
 * As outras densidades ficam a seu cargo implementar, se você quiser
 */
function getColors32(y, x, buffer, pixelpos, bitmask) {
    const value = get32(buffer, pixelpos(y, x));
    const bits = getBitmasks(bitmask);
 
    const r = getValueFromBitmask(value, bits.r);
    const g = getValueFromBitmask(value, bits.g);
    const b = getValueFromBitmask(value, bits.b);
    
    // Se não tiver alfa, faz ele ser completamente opaco.
    const a = bits.a.mask ? getValueFromBitmask(value, bits.a) : 0xff;
    // format RGBA
    return [
        r, g, b, a
    ];
}
E concluimos esse formato. Por enquanto.
O próximo formato sai daqui a alguns dias, e o repositório com um parser BMP em Javascript sai também daqui a uns dias
Vou começar a estudar a compressão RLE, mas ela vai vir mais como um bônus.