Hlavní navigace

Nepovinné chunky v PNG a kontrola pomocí CRC

5. 10. 2006
Doba čtení: 11 minut

Sdílet

V páté části seriálu, ve kterém popisujeme populární grafický formát PNG, si ukážeme způsob zabezpečení chunků pomocí CRC. Dále si pomocí jednoduché aplikace pro výpis všech chunků v souboru typu PNG vysvětlíme význam chunků bKGD, tIME, pHYs a sCAL.

Obsah

1. Zabezpečení chunků pomocí CRC
2. Utilita pro výpis všech chunků v souboru typu PNG
3. Chunk pHYs – rozměry pixelů
4. Chunk sCAL – fyzické měřítko obrázku
5. Chunk tIME – časové razítko
6. Chunk bKGD – nastavení barvy pozadí
7. Obsah dalšího pokračování tohoto seriálu

1. Zabezpečení chunků pomocí CRC

V předchozích pokračováních tohoto seriálu jsme si řekli, že každý chunk je v souboru typu PNG popsán svým jménem, délkou a také třicetidvoubitovým CRC kódem (Cyclic Redundancy Check/Cyclic Redundancy Code), který slouží pro zjištění, zda jsou data v chunku přenesena bez chyby. Oprava chyb však není pomocí CRC možná, pouze jejich detekce. Při výpočtu CRC je v případě PNG použit polynom:

x32+x26+x23+x­22+x16+x12+x11+x10+x8+x+x5+x4+x2+x+1

Třicetidvoubitové CRC kódy jsou velmi často používané v mnoha různých aplikacích, protože jsou výpočetně poměrně jednoduché (jsou použity pouze operace bitového posunu a bitové nonekvivalence nad 32bitovými registry) a přitom dokáží detekovat i mnohačetné chyby v datech. Ukazuje se však, že polynom použitý v CRC kódu u PNG má poměrně špatné vlastnosti, zejména malou Hammingovu vzdálenost – ta v podstatě určuje počet chybných bitů, které je možné detekovat. V praxi se dlouhé datové chunky (IDAT) rozdělují na několik menších chunků o velikosti typicky 8 kB, proto je pravděpodobnost vzniku nedetekované chyby poměrně malá. V případě, že by byl použit jeden dlouhý chunk (třeba o velikosti stovek kilobytů), se pravděpodobnost „proklouznutí“ chyby postupně zvyšuje.

Na stránce http://www.w3­.org/TR/PNG-CRCAppendix.html je uveden výpis algoritmu, který implementuje CRC s výše uvedeným polynomem:

   /* Table of CRCs of all 8-bit messages. */
   unsigned long crc_table[256];

   /* Flag: has the table been computed? Initially false. */
   int crc_table_computed = 0;

   /* Make the table for a fast CRC. */
   void make_crc_table(void)
   {
     unsigned long c;
     int n, k;

     for (n = 0; n < 256; n++) {
       c = (unsigned long) n;
       for (k = 0; k < 8; k++) {
         if (c & 1)
           c = 0xedb88320L ^ (c >> 1);
         else
           c = c >> 1;
       }
       crc_table[n] = c;
     }
     crc_table_computed = 1;
   }

   /* Update a running CRC with the bytes buf[0..len-1]--the CRC
      should be initialized to all 1's, and the transmitted value
      is the 1's complement of the final running CRC (see the
      crc() routine below)). */

   unsigned long update_crc(unsigned long crc, unsigned char *buf,
                            int len)
   {
     unsigned long c = crc;
     int n;

     if (!crc_table_computed)
       make_crc_table();
     for (n = 0; n < len; n++) {
       c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8);
     }
     return c;
   }

   /* Return the CRC of the bytes buf[0..len-1]. */
   unsigned long crc(unsigned char *buf, int len)
   {
     return update_crc(0xffffffffL, buf, len) ^ 0xffffffffL;
   } 

Úprava pro jiný polynom spočívá ve výměně konstanty 0×edb88320L za konstantu odlišnou. Za připomenutí stojí, že do CRC není zahrnuta délka chunku, takže výpočet CRC je možné provádět i za běhu komprimace datového chunku, u kterého dopředu neznáme jeho délku. Není tedy nutné po komprimaci obrazovými daty procházet znovu a počítat CRC, což může celé ukládání zrychlit až o 50% (při dekomprimaci již informaci o délce chunku dopředu známe). To, že délka není zabezpečena pomocí CRC, vůbec nevadí, protože při poškození samotné délky by se CRC kód vůbec nenalezl, resp. místo něj by byla přečtena jiná část souboru.

2. Utilita pro výpis všech chunků v souboru typu PNG

V následujících kapitolách tohoto článku si popíšeme některé nepovinné typy chunků, které se v obrázku mohou nacházet. Při vysvětlování jejich významu a umístění v souboru typu PNG nám pomůže následující utilita, která slouží pro postupný výpis všech chunků, které se v obrázku nachází.

Funkce utility je poměrně jednoduchá. Nejprve se otevře soubor, jehož jméno je předáno jako první (a jediný) parametr. Přeskočí se prvních osm bytů, což je hlavička PNG souboru. Posléze se v programové smyčce postupně načítají a v čitelné formě zobrazují úvodní informace o každém chunku. Jedná se o jeho délku (32 bitů) a jméno (32 bitů). Posléze se provede v souboru skok na začátek dalšího chunku. Vzhledem k tomu, že do délky chunku není zahrnut CRC, je nutné při posunu přičíst hodnotu 4 (=32 bitů).

//-----------------------------------------------------------------------------
// Utilita pro výpis všech chunků v souboru typu PNG
//-----------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>



//-----------------------------------------------------------------------------
// Výpis informací o chunku spolu s posunem v souboru
//-----------------------------------------------------------------------------
int printChunkInfo(FILE *f)
{
    unsigned char c_size[4];
    unsigned char c_name[5];
    int i_size;
    long int offset=ftell(f);
    int i;

    // načíst délku chunku
    if (!fread(c_size, 4, 1, f)) return 0;
    i_size=(c_size[0]<<24)+(c_size[1]<<16)+(c_size[2]<<8)+(c_size[3]);

    // načíst jméno chunku
    if (!fread(c_name, 4, 1, f)) return 0;
    c_name[4]=0; // ukončit řetězec pro výpis

    // zkontrolovat jméno chunku
    for (i=0; i<4; i++) {
        if (!isalpha(c_name[i])) {
            printf("v názvu chunku se vyskytuje špatný znak s kódem %x\n", c_name[i]);
            return 0;
        }
    }

    printf("offset: %08x  jméno chunku: %s   délka chunku: %d\n", offset, (char *)c_name, i_size);
    fseek(f, 4+i_size, SEEK_CUR);
    return 1;
}



//-----------------------------------------------------------------------------
// Funkce spuštěná po inicializaci procesu
//-----------------------------------------------------------------------------
int main(int argc, char *argv[])
{
    FILE *fin;

    // kontrola, zda je zadáno jméno vstupního souboru
    if (argc<2) {
        printf("Není zadáno jméno vstupního souboru (počet parametrů=%d)!\n", argc);
        return -1;
    }

    // pokus o otevření souboru
    if (!(fin=fopen(argv[1], "rb"))) {
        printf("Nelze otevřít soubor %s pro čtení!\n", argv[1]);
        return -1;
    }

    // posuneme se za hlavičku PNG
    fseek(fin, 8, SEEK_SET);

    // výpis všech nalezených chunků
    while (printChunkInfo(fin))
        ;

    // pokus o uzavření souboru
    if (fclose(fin)==EOF) {
        printf("Chyba při uzavírání souboru %s!\n", argv[1]);
    }
    return 0;
}



//-----------------------------------------------------------------------------
// finito
//----------------------------------------------------------------------------- 

Utilitu si můžeme vyzkoušet na prvním demonstračním obrázku zadáním příkazu:

png_list lena24.png > lena24.txt 

Získáme následující výsledek uložený v textovém souboru lena24.txt:

offset: 00000008  jméno chunku: IHDR   délka chunku: 13
offset: 00000021  jméno chunku: IDAT   délka chunku: 106944
offset: 0001a1ed  jméno chunku: IEND   délka chunku: 0 

png5_1

Obrázek 1: PNG s povinnými chunky IHDR, IDAT a IEND

3. Chunk pHYs – rozměry pixelů

V některých případech, například při tisku obrázků mimo prostředí webového prohlížeče či sázecího programu, může být důležité znát rozměry všech pixelů, ze kterých se obrázek skládá. Z těchto rozměrů je možné odvodit původní velikost obrázku. Kromě toho je možné z rozměru pixelů zjistit i poměr jejich výšky ku šířce. I dnes totiž existují zařízení, které neobsahují čtvercové pixely, ale pixely natažené či naopak stlačené ve směru vodorovné či svislé osy. Při zobrazování obrázku na jiných zařízeních (CRT, LCD, projektory) je pak nutné provést přepočet obrázku s ohledem na rozměry pixelů.

Informace o velikostech pixelů je uložená v nepovinném chunku nazvaném pHYs. Délka datové části tohoto chunku je rovna devíti bytům s následující strukturou:

Délka Význam
4 byty počet pixelů na jednotku horizontálně
4 byty počet pixelů na jednotku vertikálně
1 byt volba jednotky: 0 neznámá, 1 metry

Pokud je jednotka nastavena na 0, je možné z počtu pixelů zjistit pouze jejich tvar (čtverec, obdélník), pokud je jednotka nastavena na 1, jsou údaje zapsány v počtech pixelů na metr. Jedná se o jeden z mála délkových údajů v IT, který je zadán v metrické soustavě.

Tento chunk (pokud je přítomen) musí být uveden před chunkem IDAT, jak to ostatně dokazuje rozbor druhého demonstračního obrázku, který obsahuje více datových chunků rozdělených na bloky o délce 8kB (vytvořeno v GIMPu). Pokud není chunk pHYs přítomen, předpokládá se, že pixely jsou čtvercové a neznámé velikosti.

offset: 00000008  jméno chunku: IHDR   délka chunku: 13
offset: 00000021  jméno chunku: pHYs   délka chunku: 9
offset: 00000036  jméno chunku: IDAT   délka chunku: 8192
offset: 00002042  jméno chunku: IDAT   délka chunku: 8192
offset: 0000404e  jméno chunku: IDAT   délka chunku: 8192
offset: 0000605a  jméno chunku: IDAT   délka chunku: 8192
offset: 00008066  jméno chunku: IDAT   délka chunku: 8192
offset: 0000a072  jméno chunku: IDAT   délka chunku: 8192
offset: 0000c07e  jméno chunku: IDAT   délka chunku: 8192
offset: 0000e08a  jméno chunku: IDAT   délka chunku: 8192
offset: 00010096  jméno chunku: IDAT   délka chunku: 8192
offset: 000120a2  jméno chunku: IDAT   délka chunku: 8192
offset: 000140ae  jméno chunku: IDAT   délka chunku: 8192
offset: 000160ba  jméno chunku: IDAT   délka chunku: 8192
offset: 000180c6  jméno chunku: IDAT   délka chunku: 8192
offset: 0001a0d2  jméno chunku: IDAT   délka chunku: 2168
offset: 0001a956  jméno chunku: IEND   délka chunku: 0 

png5_2

Obrázek 2: PNG s povinnými chunky IHDR, IDAT a IEND a nepovinným chunkem pHYs

4. Chunk sCAL – fyzické měřítko obrázku

Určitým rozšířením výše popsaného chunku pHYs je chunk se jménem sCAL, který slouží ke specifikaci skutečné velikosti objektu, který je na obrázku zobrazen. Například se může jednat o snímek oblouky (skutečná velikost zadána v radiánech) či o mapu části země (velikost zadána v metrech). Zatímco u chunku pHYs jsme si vystačili se čtyřmi byty pro každou souřadnou osu, u chunku sCAL je zvoleno textové vyjádření dvojice číselných hodnot.

Tento chunk má proměnnou délku a formát:

Délka Význam
1 byte specifikace jednotek: 1 metry, 2 radiány
m bytů velikost v horizontálním směru ve formě řetězce
1 byte oddělovač řetězců, znak s hodnotou 0
n bytů velikost ve vertikálním směru ve formě řetězce

Možný zápis číselných hodnot je známý z většiny programovacích jazyků: 42, 1., .1, 1e10, +123.4567e-089 atd.

5. Chunk tIME – časové razítko

S fyzikální veličinou času jsou ze všech používaných fyzikálních jednotek snad největší problémy. Nejenom že je její fyzikální význam stále nejasný, problémy jsou i s jejím měřením a záznamem (přestupné roky, přebývající sekundy) a ukládáním do datových struktur na počítači (y2k, Unix time). Dokonce, jak trefně poznamenal Václav Havel, naši politici už běžně zaměňují prostor a čas (asi by jim fyzika šla lépe než to, co dělají).

Chunk nazvaný tIME se pokouší řešit problém s ukládáním času do datových struktur. Místo spoléhání se na systémový čas, který na některých systémech za několik let přeteče (Unix epoch je již za svou polovinou – http://en.wiki­pedia.org/wiki/U­nix_time) je v chunku tIME pro čas vyhrazeno celých sedm bytů s následujícím významem:

Délka Význam Rozsah hodnot
2 byte rok 0..65535
1 byte měsíc 1..12
1 byte den 1..31
1 byte hodina 0..23
1 byte minuta 0..59
1 byte sekunda 0..60

Z uvedených rozsahů je patrné, že časový údaj uložený v obrázku typu PNG jen tak rychle nepřeteče. Za povšimnutí stojí rozsah sekund 0..60 místo očekávaného 0..59. Je to z důvodu možného uložení tzv. přebývajících sekund (leap second), kterými se jednou za několik let upravuje oficiální čas (UTC) podle pohybu Země (http://en.wi­kipedia.org/wi­ki/Leap_secon­d).

Podle specifikace by v tomto chunku měly být uloženy informace o poslední modifikaci obrazových dat, tj. čas by se neměl měnit při úpravách metadat či jiné manipulaci s PNG, která obrázek nemění (optimalizace pomocí programu pngcrush apod.). Bývá dobrým zvykem ukládat chunk tIME na začátek souboru, i když jeho umístění je možné kdekoli. Chunk může být v souboru umístěn pouze jedenkrát, jak je ukázáno na následujícím příkladu (viz třetí demonstrační obrázek):

offset: 00000008  jméno chunku: IHDR   délka chunku: 13
offset: 00000021  jméno chunku: pHYs   délka chunku: 9
offset: 00000036  jméno chunku: tIME   délka chunku: 7
offset: 00000049  jméno chunku: IDAT   délka chunku: 8192
offset: 00002055  jméno chunku: IDAT   délka chunku: 8192
offset: 00004061  jméno chunku: IDAT   délka chunku: 8192
offset: 0000606d  jméno chunku: IDAT   délka chunku: 8192
offset: 00008079  jméno chunku: IDAT   délka chunku: 8192
offset: 0000a085  jméno chunku: IDAT   délka chunku: 8192
offset: 0000c091  jméno chunku: IDAT   délka chunku: 8192
offset: 0000e09d  jméno chunku: IDAT   délka chunku: 8192
offset: 000100a9  jméno chunku: IDAT   délka chunku: 8192
offset: 000120b5  jméno chunku: IDAT   délka chunku: 8192
offset: 000140c1  jméno chunku: IDAT   délka chunku: 8192
offset: 000160cd  jméno chunku: IDAT   délka chunku: 8192
offset: 000180d9  jméno chunku: IDAT   délka chunku: 8192
offset: 0001a0e5  jméno chunku: IDAT   délka chunku: 2168
offset: 0001a969  jméno chunku: IEND   délka chunku: 0 

png5_3

Obrázek 3: PNG s povinnými chunky IHDR, IDAT a IEND a nepovinnými chunky pHYs a tIME

6. Chunk bKGD – nastavení barvy pozadí

Při postupném zobrazování obrázku, například ve webovém prohlížeči, je vhodné, aby se obdélník, do kterého se obrázek vykresluje, nejprve vybarvil vhodnou barvou. Pro specifikaci této barvy slouží chunk bKGD. Význam tohoto chunku zhruba odpovídá funkci bytu background_index v hlavičce obrázku typu GIF, ovšem s tím rozdílem, že u GIFu nebylo nutné celý obdélník „logické obrazovky“ překreslit, kdežto u PNG je vždy překreslen celý obdélník (s případnou průhledností).

Chunk bKGD má rozdílnou délku podle toho, o jaký typ obrázku se jedná. U PNG obrázku s barvovou paletou je délka datové části chunku rovna pouhému jednomu byte, protože data obsahují pouze index barvy pozadí v barvové paletě. Pro obrázky uložené ve stupních šedi je délka dat rovna dvěma bytům (rozsah šedi je 0..216-1) a konečně u plnobarevných (truecolor) obrázku je barva pozadí zapsána šesticí bytů:

Délka Význam
2 byty červená barvová složka, rozsah 0..2bitová hloubka-1
2 byty zelená barvová složka, rozsah 0..2bitová hloubka-1
2 byty modrá barvová složka, rozsah 0..2bitová hloubka-1

V případě, že je v obrázku pro každou barvovou složku použito pouze osmi bitů, je při specifikaci barvy pozadí spodních 8 bitů každé barvové složky ignorováno. Příklad umístění chunku bKGD v obrázku typu PNG:

offset: 00000008  jméno chunku: IHDR   délka chunku: 13
offset: 00000021  jméno chunku: bKGD   délka chunku: 6
offset: 00000033  jméno chunku: pHYs   délka chunku: 9
offset: 00000048  jméno chunku: tIME   délka chunku: 7
offset: 0000005b  jméno chunku: IDAT   délka chunku: 8192
offset: 00002067  jméno chunku: IDAT   délka chunku: 8192
offset: 00004073  jméno chunku: IDAT   délka chunku: 8192
offset: 0000607f  jméno chunku: IDAT   délka chunku: 8192
offset: 0000808b  jméno chunku: IDAT   délka chunku: 8192
offset: 0000a097  jméno chunku: IDAT   délka chunku: 8192
offset: 0000c0a3  jméno chunku: IDAT   délka chunku: 8192
offset: 0000e0af  jméno chunku: IDAT   délka chunku: 8192
offset: 000100bb  jméno chunku: IDAT   délka chunku: 8192
offset: 000120c7  jméno chunku: IDAT   délka chunku: 8192
offset: 000140d3  jméno chunku: IDAT   délka chunku: 8192
offset: 000160df  jméno chunku: IDAT   délka chunku: 8192
offset: 000180eb  jméno chunku: IDAT   délka chunku: 8192
offset: 0001a0f7  jméno chunku: IDAT   délka chunku: 2168
offset: 0001a97b  jméno chunku: IEND   délka chunku: 0 

root_podpora

png5_4

Obrázek 4: PNG s povinnými chunky IHDR, IDAT a IEND a nepovinnými chunky bKGD, pHYs a tIME

7. Obsah dalšího pokračování tohoto seriálu

V závěrečné části seriálu o grafickém formátu PNG si popíšeme zbývající důležité chunky, zejména ty, které se týkají barvových profilů a ukládání textových informací.

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.