Ukládání BMP obrázků (MFC a C++)

Potřebovali jste někdy ve svých programech ukládat obrázky do souboru a nevěděli si s tím rady? Pokud ano, tak se vám dnešní článek bude určitě hodit. Naučíme se ukládat obrázky typu 24-bitové BMP.

Nejprve se tedy seznámíme se strukturou BMP souboru (24-bitů):

SOUBOR BMP
{
  BITMAPFILEHEADER  //struktura s identifikaci souboru atd.
  BITMAPINFOHEADER //struktura obsahuje vlastnosti obrázku
  obrazová data
}

Popíšeme si jednotlivé struktury.

1. BITMAPFILEHEADER

typedef struct tagBITMAPFILEHEADER {
  WORD    bfType;
  DWORD   bfSize;
  WORD    bfReserved1;
  WORD    bfReserved2;
  DWORD   bfOffBits;
} BITMAPFILEHEADER;

Na začátku souboru jsou první dva bajty, které identifikují soubor jako BMP a musí být vždy nastaveny na "BM". Dále následuje proměnná bfSize , ve které je uložena velikost obrazových dat. Položky bfReserved1 a bfReserved2 stačí nastavit na 0 - jsou pouze rezervované. A nakonec v bfOffBits musí být uložen posun od začátku struktury BITMAPFILEHEADER po samotná obrazová data.

2. BITMAPINFOHEADER

typedef struct tagBITMAPINFOHEADER{
  DWORD  biSize;
  LONG   biWidth;
  LONG   biHeight;
  WORD   biPlanes;
  WORD   biBitCount
  DWORD  biCompression;
  DWORD  biSizeImage;
  LONG   biXPelsPerMeter;
  LONG   biYPelsPerMeter;
  DWORD  biClrUsed;
  DWORD  biClrImportant;
} BITMAPINFOHEADER;

Do proměnné biSize dosadíme velikost struktury BITMAPINFOHEADER, biWidth a biHeight specifikují šířku a výšku obrázku v pixelech a bmPlanes udává počet bitových rovin - musí být 1. Do biBitCount dosadíme počet bitů na pixel, v našem případě 24, biCompression udává kompresi obrázku. My budeme pracovat s nekomprimovanými obrázky, a proto dáme BI_RGB. biSizeImage je velikost obrázku v bitech, dá se nastavit na 0, ale já to budu počítat. Další dvě proměnné biXPelsPerMeter a biYPelsPerMeter udávají horizontální a vertikální rozlišení obrázku, já nastavuji vždy na 96. biClrUsed udává, kolik barev je použitých v bitmapě. Když je nula, tak udává, že jsou použity všechny. Poslední biClrImportant je počet barev, které jsou důležité pro správné vykreslení bitmapy, opět zadáme 0.

3. Obrazová data

Po obou dvou informačních hlavičkách hned následují vlastní obrazová data. Jak jsou uložena závisí hlavně na barevné hloubce obrázku. A jelikož my budeme používat 24-bitové obrázky, bude uspořádání následující:

Obrazová data se dělí na tzv. scan řádky, kde jeden scan řádek obsahuje data jednoho řádku obrázku. Ale pozor, délka scan řádku musí být vždy zarovnána na 4 bajty, to znamená, že když je délka obrazového řádku např. 59 bajtů, scan řádek bude dlouhý 60 bajtů. Další zvláštnost je, že scan řádky jsou v souboru uloženy pozpátku.

V jednotlivých scan řádcích jsou už pak přímo uložena obrazová data. Definice jednoho pixelu na obrázku se při 24-bitovém formátu skládá ze tří bajtů - červené, modré a zelené. V souboru někdy bývají uloženy tak, že je červená a zelená prohozená, ale ještě se mi nepodařilo zjistit, kdy ano a kdy ne. Délku jednoho scan řádku tedy vypočítáme tak, že šířku obrázku v pixelech vynásobíme třemi a pak ještě zarovnáme na 4 bajty. Postupně se takto uloží všechny řádky a soubor je na světě.

Postup při ukládání

Myslím, že teorie už bylo dost, a proto uvedu rovnou celý zdrojový kód. Třída CMemDC je rozšířením třídy CDC a umožňuje mnohem rychleji provádět funkce GetPixel a SetPixel .

#include "memDC.h"   //hlavičkový soubor pro CMemDC

void Save(CDC* pDC,int x,int y,int sirka,int vyska,LPCTSTR FileName)
{
  CMemDC dcMem;
  dcMem.Create(sirka,vyska,pDC);
  dcMem.BitBlt(x,y,sirka,vyska,pDC,0,0,SRCCOPY);
  
  BITMAPFILEHEADER	bmFileHeader;
  BITMAPINFOHEADER	bmInfoHeader;
  int i,j,c;
  DWORD ByteCount;
  FILE* bmpFile;
  BYTE *pBuffer;
  COLORREF color;
  
  UINT DelkaBuf=((3*sirka+3)>>2)<<2;
  pBuffer=(BYTE *)malloc(DelkaBuf * sizeof(BYTE) );
  if((bmpFile=fopen(FileName,"wb"))==NULL)
    return;
    
  bmFileHeader.bfType=('B'|('M'<<8));
  bmFileHeader.bfSize=0; //později se opraví
  bmFileHeader.bfReserved1=bmFileHeader.bfReserved2=0;
  bmFileHeader.bfOffBits=sizeof(BITMAPFILEHEADER)+
    sizeof(BITMAPINFOHEADER);
  bmInfoHeader.biSize = sizeof( BITMAPINFOHEADER );
  bmInfoHeader.biWidth = sirka;
  bmInfoHeader.biHeight = vyska;
  bmInfoHeader.biPlanes = 1;
  bmInfoHeader.biBitCount = 24;
  bmInfoHeader.biCompression = BI_RGB;
  bmInfoHeader.biSizeImage = 0; //později se opraví
  bmInfoHeader.biXPelsPerMeter = 96;
  bmInfoHeader.biYPelsPerMeter = 96;
  bmInfoHeader.biClrUsed = 0;
  bmInfoHeader.biClrImportant = 0;
  
  fwrite( &bmFileHeader, sizeof( BITMAPFILEHEADER ), 1, bmpFile );
  fwrite( &bmInfoHeader, sizeof( BITMAPINFOHEADER ), 1, bmpFile );
  
  ByteCount = 0;
  for( i=y+vyska-1; i >= y; i-- )
  {
    for( j=x, c=0; j < x+sirka; j++, c++ )
    {
      color=dcMem.ZjistiBarvuPixelu(j,i);
      pBuffer[c*3+2] = GetRValue(color);
      pBuffer[c*3+1] = GetGValue(color);
      pBuffer[c*3] = GetBValue(color);
      ByteCount+=3;
    }
    
    if( fwrite(pBuffer, sizeof(BYTE),DelkaBuf, bmpFile)!=DelkaBuf )
    {
      free( pBuffer );
      return;
    }
  }
  
  bmInfoHeader.biSizeImage = ByteCount; //oprava
  bmFileHeader.bfSize = ByteCount + sizeof( BITMAPFILEHEADER )+
    sizeof( BITMAPINFOHEADER );
  fseek( bmpFile, 0, SEEK_SET );
  fwrite( &bmFileHeader, sizeof( BITMAPFILEHEADER ), 1, bmpFile );
  fwrite( &bmInfoHeader, sizeof( BITMAPINFOHEADER ), 1, bmpFile );
  fclose( bmpFile);
  
  free( pBuffer );
}

Popis kódu:

Úplně na začátku se vkládá soubor memDC.h kvůli třídě CMemDC. Dále je deklarace proměnných a vypočítání délky scan řádku. Tím, že provedeme nejprve bitový posun doprava a pak doleva, dosáhneme toho, že výsledek bude násobkem čtyř. Potom otevíráme soubor na zápis, nastaví se jednotlivé proměnné ve strukturách a uloží se obě hlavičky. Teď přichází na řadu asi ta nejzajímavější část kódu. Postupně se procházejí jednotlivé pixely v pořadí od levého spodního do pravého horního. Z každého pixelu se pak získají jednotlivé složky barev a ty se v obráceném pořadí uloží do bufferu. Vždy, když je jeden řádek přečtený, buffer se uloží do souboru. Zároveň s ukládáním dat se také zvětšuje proměnná ByteCount , jejíž hodnota se potom přepíše v hlavičce souboru. Nakonec se paměť bufferu uvolní a uzavře se soubor, se kterým jsme pracovali.

Nyní už tedy víte, jak vše pracuje, ale je tu jeden problém. Bmp obrázky jsou totiž ukládány nekomprimované, to znamená, že zabírají hrozně moc místa. Proto jsem se rozhodl , že k tomuto článku napíši ještě pokračování, které se bude týkat ukládání PCX obrázků. Ale to až příště.

Stáhnout zdrojový kód (29kb)

UPOZORNĚNÍ: Jedná se o archiv článků z let 2003 - 2005, uvedené technologie či postupy již mohou být neaktuální.