Hlavní navigace

Jak vytvořit jednoduchý rastrový obrázek v jazyce C

3. 2. 2004
Doba čtení: 9 minut

Sdílet

Tento článek shrnuje moje zkušenosti a znalosti, jak vytvořit z C/C++ rastrový obrázek v PNG. V žádném případě si nekladu za cíl podat ucelený popis formátu PNG a jeho možností, ale spíš jako velmi rychlý a praktický úvod do problematiky tvoření rastrových obrázků z C/C++, například grafů apod.

Tak jsem objevil Ameriku a trvalo mi to týden.

Již několikrát jsem byl postaven před problém, jak doplnit výstupy programu nějakým obrázkem znázorňujícím třeba graf apod. Zatím jsem to řešil přes externí program typu gnuplot, gnumeric nebo OOffice. Nyní jsem však potřeboval napsat program, který vytvoří rastrový obrázek kruhové clonky pro optickou trať s plynule se měnící propustností světla, a tady jsem pohořel se všemi nástroji, co znám. Nakonec mi zbylo C a idea vytvořit obrázek pomocí knihovny libpng. Vůbec přijít na to, co mám vlastně hledat, pročíst manuálové stránky, dohledat a pochopit příklady kódu na internetu a přijít na ten zbytek, který jsem se nikde nedočetl, mi zabralo přesně týden, a přitom je to tak jednoduché. Přečíst tento článek by nemělo zabrat více než dvě hodiny a bude to snad jasné :)

Definice problému: Jak vytvořit obrázek ve stupních šedi, když znám rozměry obrázku a barvu (stupeň šedi) každého pixelu.

Použité nástroje: Jazyk C/C++ a jeho překladač (na Linuxu nejspíše gcc), knihovna libpng verze 1.2.2 a vyšší. Verze libpng není podstatná, maximálně bude mít nějaký příkaz nepatrně jinou syntaxi, ale to už snadno dohledáte v man stránce.

PNG formát, něco málo o něm a o libpng

Jako první je potřeba vědět něco malounko o formátu PNG. Jedná se o rastrový formát s podporou komprese, optimalizací, různých bitových hloubek (počtu barev) a se spoustou dalších možností. Obrázek obsahuje hlavičku a za ní následují data obrázku. Hlavička obsahuje informace o rozměrech obrázku, stupni a způsobu komprese, bitové hloubce apod. Nejdůležitější je asi pochopení pojmů barevná hloubka (nebo bitová hloubka), stupně šedi a RGB barvy.

Začneme barevnou hloubkou. Barva každého bodu je definována nějakým číslem a jde o to, jak velké to číslo může maximálně být.

  • Černobílý obrázek: každý pixel je definován hodnotou 0 (černá), nebo 1 (bílá). V takovémto obrázku je každý pixel uložen v jednom bit (česky „bitu“). Barevná hloubka je jeden bit. Takže v každém byte (krásně česky, ať je bordel, „bajtu“) je uloženo osm pixelů.
  • 16barevný obrázek: každý pixel je uložený ve 4 bit (24 = 16), takže jeden byte obsahuje již jen dva pixely.
  • 256 barev = 8 bit (jeden byte, jeden pixel na byte).
  • 65536 barev = 16 bit (dva byte, půl pixelu na byte).
  • Stupně šedi: Pokud je obrázek v hlavičce označen jako „gray scale“, hodnota pixelu označuje stupeň šedi. 0 znamená černou, max. číslo = bílá (např. pro 8 bit hloubku je bílá = 28 – 1 = 255).
  • RGB barvy: k popisu barvy se používá klasický barevný trojúhelník červená-zelená-modrá (Red-Green-Blue). Takže každému pixelu odpovídají tři za sebou jdoucí čísla, jejichž vzájemný poměr definuje výslednou barvu. Takže pokud použijete 8 bit RGB, znamená to, že jeden pixel zabírá 3× 8 bit, neboli 24 bitů, a kombinace pak dává 16.7 miliónů barev. Pro 3× 16 bit, neboli 48 bitů, dokonce stráááášně moc barev, to snad už ani nemá smysl.

Další otázka je, co lze od knihovny libpng očekávat. Knihovna libpng se zabývá pouze načtením obrázku ze souboru, zápisem obrázku do souboru a několika základními operacemi transformace barev apod. Nic víc. Transformacemi obrázku a načítáním ze souboru se zabývat nebudeme, my ho chceme vytvořit. Knihovně je třeba v podstatě předhodit rastr obrázku v paměti a vyplnit údaje do hlavičky. O ostatní se postará knihovna.

Příklad vytvoření obrázku (8 bit hloubka)

priklad

Konec teorie a následují praktické ukázky kódu. Kód není psán tak, aby stačil zkopírovat a jel, je psám pro jasné pochopení na první čtení. Nutné úpravy jsou snad každému céčkaři jasné na první pohled. Jde mi o pochopení toho, co a proč je potřeba kde udělat.

Jak může vypadat typický rastr šedého obrázku v paměti? Třeba jako dvourozměrné unsigned int pole. Typ by měl být rozhodně celočíselný a neznaménkový, barva je celé číslo rovné nebo větší než nula. Velikost typu by měla být dostatečná, aby se tam rozsahem vešlo číslo 2^bit_depth.

Následující příklad je návod, jak vytvořit primitivní obrázek ve stupních šedi.

/* stupně šedi, a proto depth = bit pixel */
#define BIT_PIXEL 8
#define HEIGHT 100
#define WIDTH  100

sizeof (typ pole) musí být >= (BIT_PIXEL / 8), takže pro jistotu je třeba:

unsigned long int rastr[HEIGHT][WIDTH];

Nyní pole naplníme nějakou barvou, třeba 20% šedou, a přidáme doprostřed čárku:

for (i = 0; i < HEIGHT; i++)
  for (j = 0; j < WIDTH; j++)
    rastr[i][j] = lrint ((1 - 0.2) *
                   ((1 << BIT_PIXEL) - 1));
rastr[48][48] = BLACK;
rastr[49][49] = BLACK;
rastr[50][50] = BLACK;
rastr[51][51] = BLACK;
rastr[52][52] = BLACK;

Takže rastr by byl a můžeme jít zapisovat pomocí libpng. K tomu potřebujeme hlavičkový soubor:

#include <png.h>

Dále potřebujeme některé proměnné pro práci s png formátem:

/* png_ptr - pro vnitřní použití libpng */
png_structp png_ptr = NULL;
/* info_ptr - informační struktura o png */
png_infop info_ptr = NULL;

Dále je potřeba řádka obrázku

png_bytep p_row = NULL; /* v podstatě char * p_row */
png_text pngtext[1];    /* komentář k obrázku */

Nejdříve je třeba otevřít soubor pro binární zápis:

FILE * fp;
fp = fopen ("soubor.png", "wb");
if (fp == NULL)
  {
    chyba při otevírání/vytváření 'soubor.png',
    ošetřit ...
  }

/* pak vytvořit png vnitřní strukturu */
png_ptr = png_create_write_struct
           (PNG_LIBPNG_VER_STRING,
            (png_voidp) NULL, NULL, NULL);
if (png_ptr == NULL)
  {
    fclose (fp);
    return (1);
  }

/* pak informační strukturu */
info_ptr = png_create_info_struct (png_ptr);
if (info_ptr == NULL)
  {
    fclose (fp);
    png_destroy_write_struct (&png_ptr,
             png_infopp_NULL);
    return (1);
  }

/* odchytit vnitřní chyby png lib ... */
if (setjmp (png_jmpbuf (png_ptr)))
  {
    fclose (fp);
    png_destroy_write_struct (&png_ptr, &info_ptr);
    return (1);
  }

/* I/O inicializace ... */
png_init_io (png_ptr, fp);

Takže teď je připraven soubor pro zápis a jsou vytvořeny základní struktury pro vytvoření obrázku.

Nyní je potřeba připravit hlavičku. Výška a šířka musí být menší než 231, barevná hloubka … Parametry jsou snad dostatečně samovysvětlující. Snad jen připomínka, v libpng 1.2.2 musí být komprese a typ filtru nastaveny na DEFAULT jako v tomto příkladu na následujících řádcích:

png_set_IHDR (png_ptr, info_ptr, WIDTH, HEIGHT, BIT_PIXEL,
              PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE,
              PNG_COMPRESSION_TYPE_DEFAULT,
              PNG_FILTER_TYPE_DEFAULT);

/* Zapsat komentář */
pngtext[0].key = "Title";
pngtext[0].text = "Priblbly obrazek";
pngtext[0].compression = PNG_TEXT_COMPRESSION_NONE;

# ifdef PNG_iTXt_SUPPORTED
  pngtext[0].lang = NULL;
# endif

png_set_text (png_ptr, info_ptr, pngtext, 1);

/* Mno a konečně zapsat hlavičku */
png_write_info (png_ptr, info_ptr); 

Až potud vše najdete v dokumentaci k libpng dosti podrobně a slušně popsáno včetně vzorového příkladu. Následující část, jak naplnit samotné tělo obrázku, tam však již není :(

Ještě jsme nenaalokovali řádku p_row pro zápis obrázku. Tady bude drobný problém, C umí alokovat paměť v byte, ale řádka by při špatné konstelaci hvězd mohla přetéct celých n byte o pár bit, např. pokub by BIT_PIXEL == 1, 2, nebo 4 a WIDTH by bylo prvočíslo. To řeší následující konstrukce přes pomocnou proměnnou i:

if ((WIDTH * sizeof (png_byte) * BIT_PIXEL % 8) == 0)
  i = WIDTH * sizeof (png_byte) * BIT_PIXEL / 8;
else
  i = (WIDTH * sizeof (png_byte) * BIT_PIXEL / 8) + 1; 

Mno prima a teď konečně ta alokace a ošetření případné chyby:

p_row = malloc ((size_t) i);
if (p_row == NULL)
  {
    fclose (fp);
    png_destroy_write_struct (&png_ptr, &info_ptr);
    return (1);
  }

A jde se zapisovat samotný obrázek hezky řádku po řádce:

for (i = 0; i < HEIGHT; i++)
  {
    for (j = 0; j < WIDTH; j++)
      {
        /* naplnit řádku hezky pixel po pixelu: */
        /* POZOR - tento kus kódu bude fungovat,
         * jen pokud bude BIT_PIXEL = 8.
         * diskuse, proč, a ukázka, jak i na
         * větší/menší bitové hloubky, viz níže */
        p_row[j] = (png_byte) rastr[i][j];
      }
    /* zapsat řádku */
    png_write_row (png_ptr, p_row);
  }

/* zakončit png soubor */
png_write_end (png_ptr, info_ptr);

/* úklid a odchod */
free (p_row);
png_destroy_write_struct (&png_ptr, &info_ptr);
fclose (fp);
return (0);

Tak a to je tak nějak vše, přátelé.

Dobře, máte pravdu, není. Při plnění řádky je poznámka, že to bude fungovat, jen pokud BIT_PIXEL == 8, to znamená 1 byte. Problém je v tom přiřazení

p_row[j] = (png_byte) rastr[i][j];

Tam se vezme nejnižší byte z buňky pole rastr a vloží se do buňky p_row. Jenže pokud v rastr[i][j] je číslo delší/kratší než celý byte, je tu problém.

16bitová hloubka

Takže co když používáme 16 bit hloubku? Pak je potřeba barvu uložit do dvou buněk p_row. Jtý prvek řádky je tedy uložen v p_row[2j] a p_row[2j + 1]. Pro rozhození barvy rastr[i][j] z čísla na jednotlivé byte je potřeba bitová aritmetika. Do p_row[2j] je potřeba vložit horní z posledních dvou byte čísla rastr[i][j] a do p_row[2j + 1] dolní byte. Bude se hodit makro:

#define FULL_BYTE 255 /* bits: 11111111 */

Nyní „sešoupneme“ rastr[i][j] o jeden byte dolů a ještě je dobré ho pro jistotu bitově přenásobit FULL_BYTE:

p_row[2*j] = (png_byte) ((rastr[i][j] >> 8) & FULL_BYTE); 

A pro druhý byte p_row není potřeba nic posouvat, stačí jen přenásobit.

p_row[2*j + 1] = (png_byte) (rastr[i][j] & FULL_BYTE); 

4bitová hloubka

Problém je obrácený. V jedné buňce p_row jsou zapsány dva pixely. První pixel (rastr[i][j], j je sudé, j běží od 0 do WIDTH-1) je potřeba „vyšoupnout“ o 4 bit nahoru. Druhý pixel (liché j) stačí přenásobit spodní půlkou „jedničkového“ byte a vložit na místo. Příklad:

#define MS_BYTE   240    /* bits: 11110000 */
#define LS_BYTE    15    /* bits: 00001111 */

dva pixely v jedné buňce, tedy i index poloviční

unsigned long int k;
k = j / 2;
/* zbytek po dělení - test sudé/liché */
if ((j % 2) == 0)
  {
    /* sudé, tedy první pixel, horní půlka
     * p_row[k]. Nejdřív je potřeba ji vynulovat
     * pomocí násobení s LS_BYTE a pak připočíst
     * rastr[i][j] šouplý o 4 nahoru */

    p_row[k] = (png_byte) ((p_row[k] & LS_BYTE) |
                 ((rastr[i][j] << 4) & MS_BYTE));
else
  {
    /* liché, tedy druhý pixel, dolní půlka
     * p_row[k]. Nejdřív je potřeba ji vynulovat
     * pomocí násobení s MS_BYTE a pak připočíst
     * rastr[i][j] */

    p_row[k] = (png_byte) ((p_row[k] & MS_BYTE) |
                 (rastr[i][j] & LS_BYTE));
  }

Méně a více bitová hloubka už je pak analogická. RGB barvy … jestli jste dočetli až sem, tak asi nevidíte problém. Teda ještě vědět, jaká barva z jaké kombinace vyleze. Pro technické obrázky si ale člověk stejně obvykle vystačí s indexovanými 16 barvami, nebo snad ne?

Čárovou grafiku již dnes nemám sílu popisovat, nějak jsem neodhadl, jak se ten text rozroste. Takže kdo má zájem, koukněte se sami třeba do zdrojáků gnuplotu. Je to jednoduché, viz odkaz dole.

ict ve školství 24

Odkazy, zdroje, zajímavá místa a příklady:

  • man 3 libpng – podrobný popis knihovny libpng
  • libpng – soubor example.c v adresáři s dokumentací
  • zdrojové kódy programu gnuplot: ftp.gnuplot.in­fo/pub/gnuplot/ Konkrétně ve verzi 3.7.3 soubory bitmap.c (čárová grafika ve funkci b_line()) a term/png.trm (funkce PNG_text())
  • Celý příklad včetně vší bižuterie a správně seřazený přímo pro překlad: priklad.c, stejně tak Makefile a vzorový výsledný obrázek priklad.png.

Snad to někomu k něčemu bude. Přeji hodně úspěchů při bouchání kódu.

Autor článku