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+x22+x16+x12+x11+x10+x8+x7+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
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
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.wikipedia.org/wiki/Unix_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.wikipedia.org/wiki/Leap_second).
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
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
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í.