Obsah
1. Manipulace s binárními datovými strukturami v Pythonu (2. část)
2. Metoda Struct.unpack pro přečtení hodnot z binární struktury
3. Korektní přečtení jedné hodnoty z binárních dat
4. Čtení vícebajtové hodnoty se specifikací pořadí bajtů
5. Binární formáty obsahující více hodnot různých typů
7. Výpočet velikosti binární struktury se zadaným formátem prvků
8. Velikost binární struktury s výplněmi
9. Doplňkové bajty na konci binární struktury
10. Krátké zopakování z minula: zápis rastrového obrázku do formátu PNG
12. Testovací obrázek, který budeme načítat a analyzovat
13. Čtení signatury souborů PNG
14. Načtení hlaviček jednotlivých chunků
15. Přeskok datové části chunku a přečtení kontrolního součtu
16. Úplný zdrojový kód příkladu, který přečte chunky uložené v souboru formátu PNG
17. Načtení informací uložených v chunku IHDR
18. Úplný zdrojový kód skriptu, který vypíše strukturu PNG souboru i přesné informace z hlavičky
19. Repositář s demonstračními příklady
1. Manipulace s binárními datovými strukturami v Pythonu (2. část)
Na první článek o zpracování binárních dat v programovacím jazyku Python dnes navážeme. Zabývat se budeme problematikou zarovnání údajů v datových strukturách uložených v binární podobě (alignment) a taktéž tím, jak jsou realizovány výplně (padding) přidávané na konec serializovaných datových struktur. Jedná se o poměrně důležitá témata, protože se se zarovnanými strukturami popř. se strukturami s doplněnými bajty můžeme setkat relativně často, zejména při posílání dat mezi Pythonem a překládanými jazyky typu C, Go či Rust. A v závěrečné části článku si ukážeme způsob čtení informací z datového formátu PNG, do kterého již umíme data ukládat.
2. Metoda Struct.unpack pro přečtení hodnot z binární struktury
Pro serializaci hodnot do binárních dat jsme v předchozím článku používali funkci unpack ze standardního balíčku struct. Opačná operace, tj. získání hodnoty určitého typu z binárních dat, je nepatrně složitější, neboť namísto volání jedné funkce je nejprve nutné zkonstruovat objekt typu struct.Struct (s předáním požadovaného formátu) a následně zavolat metodu unpack, které se předají binární data (bytes, bytearray atd.). Tato metoda z binárních dat získá požadovanou hodnotu či hodnoty a vrátí je formou n-tice (tuple):
unpack(self, buffer, /) unbound _struct.Struct method Return a tuple containing unpacked values. Unpack according to the format string Struct.format. The buffer's size in bytes must be Struct.size.
Celý postup si ukážeme na jednoduchém demonstračním příkladu, v němž nejprve uložíme celočíselnou hodnotu 42 do binárních dat (typ bytes) a pro jistotu si obsah binárních dat vypíšeme. Následně se zkonstruuje objekt typu struct.Struct předáním požadovaného formátu do konstruktoru (formát „b“ – jediný bajt). Posledním krokem je zavolání metody Struct.unpack(), které předáme již dříve vytvořená binární data:
import struct # uložení hodnoty bajtu do binární struktury bytes = struct.pack("b", 42) print("Serialized: ", bytes.hex(" ", 1)) # přečtení binární struktury, která obsahuje jediný bajt s = struct.Struct("b") from_struct = s.unpack(bytes) # vypsat přečtenou hodnotu print("Deserialized:", from_struct) print("Type: ", type(from_struct))
Výsledkem deserializace z binárních dat bude n-tice obsahující jediný prvek, konkrétně prvek s celočíselnou hodnotou 42:
Serialized: 2a Deserialized: (42,) Type: <class 'tuple'>
3. Korektní přečtení jedné hodnoty z binárních dat
V případě, že se z binárních dat čte pouze jediný údaj, typicky znak, celočíselná numerická hodnota, numerická hodnota s plovoucí řádovou čárkou atd., je řešení velmi jednoduché – metodou Struct.unpack si necháme vrátit n-tici, která ovšem bude obsahovat jediný údaj (specifikovaný ve formátu). Následně z n-tice tento jediný její prvek přečteme a získáme tak hodnotu korektního typu.
Celý postup je snadný a můžeme si ho ověřit na následujícím demonstračním příkladu:
import struct # uložení hodnoty bajtu do binární struktury bytes = struct.pack("b", 42) print("Serialized: ", bytes.hex(" ", 1)) # přečtení binární struktury, která obsahuje jediný bajt s = struct.Struct("b") # první prvek z přečtené struktury from_struct = s.unpack(bytes)[0] # vypsat přečtenou hodnotu print("Deserialized:", from_struct) print("Type: ", type(from_struct))
Tento skript by po svém spuštění měl vypsat, že binární data obsahují jediný bajt s hodnotou 0×2a a následně se navíc vypíše, že byl tento bajt deserializován do celočíselné hodnoty 42, která má korektní (resp. přesněji řečeno očekávaný) typ int:
Serialized: 2a Deserialized: 42 Type: <class 'int'>
4. Čtení vícebajtové hodnoty se specifikací pořadí bajtů
Již minule jsme se setkali s problematikou pořadí bajtů u vícebajtové hodnoty uložené do binárního bloku. Víme již, že pořadí uložení bajtů lze při serializaci (tj. při volání funkce struct.pack) ovlivnit pomocí znaků <, >, ! atd. zapisovaných do řetězce s formátem. Tyto znaky se zapisují na samotný začátek řetězce s formátem:
Znak | Význam |
---|---|
@ | podle platformy (ovlivňuje i zarovnání atd.) |
= | podle platformy, ovšem bez zarovnání |
< | little endian |
> | big endian |
! | big endian (zde se ovšem používá označení „network“) |
Tytéž znaky je možné použít i při konstrukci objektu struct.Struct a zvolit tak, jak budou vícebajtové hodnoty načteny (deserializovány) z binárních dat. Ukažme si nejprve deserializaci dvoubajtové hodnoty typu celé číslo v případě, že nebudeme specifikovat pořadí bajtů:
import struct # uložení hodnoty bajtu do binární struktury bytes = struct.pack("h", 42) print("Serialized: ", bytes.hex(" ", 1)) # přečtení binární struktury, která obsahuje jediný bajt s = struct.Struct("h") # první prvek z přečtené struktury from_struct = s.unpack(bytes)[0] # vypsat přečtenou hodnotu print("Deserialized:", from_struct) print("Type: ", type(from_struct))
Na platformě x86(64) by se měly po spuštění tohoto skriptu zobrazit následující hodnoty (povšimněte si, že nejdříve je uložen nižší bajt):
Serialized: 2a 00 Deserialized: 42 Type: <class 'int'>
Samozřejmě můžeme explicitně nastavit, že budeme číst dvoubajtovou hodnotu uloženou v pořadí bajtů little endian:
import struct # uložení hodnoty bajtu do binární struktury bytes = struct.pack("h", 42) print("Serialized: ", bytes.hex(" ", 1)) # přečtení binární struktury, která obsahuje jediný bajt s = struct.Struct("<h") # první prvek z přečtené struktury from_struct = s.unpack(bytes)[0] # vypsat přečtenou hodnotu print("Deserialized:", from_struct) print("Type: ", type(from_struct))
Výsledky budou na platformě x86(64) totožné:
Serialized: 2a 00 Deserialized: 42 Type: <class 'int'>
Nebo naopak při čtení specifikujeme pořadí bajtů big endian:
import struct # uložení hodnoty bajtu do binární struktury bytes = struct.pack("h", 42) print("Serialized: ", bytes.hex(" ", 1)) # přečtení binární struktury, která obsahuje jediný bajt s = struct.Struct(">h") # první prvek z přečtené struktury from_struct = s.unpack(bytes)[0] # vypsat přečtenou hodnotu print("Deserialized:", from_struct) print("Type: ", type(from_struct))
Nyní se přečte odlišná hodnota, konkrétně hodnota odpovídající 42×256:
Serialized: 2a 00 Deserialized: 10752 Type: <class 'int'>
5. Binární formáty obsahující více hodnot různých typů
V praxi se velmi často dostaneme do situace, v níž je nutné serializovat nebo deserializovat rozsáhlejší datové struktury obsahující hodnoty různých typů. Připomeňme si například, jakým způsobem se zapsala hlavička IHDR do souboru ve formátu PNG:
struct.pack("!2I5B", width, height, 8, 2, 0, 0, 0)
V tomto případě se zapíše dvojice čtyřbajtových celých čísel a následně pětice bajtů.
Ovšem právě u podobných datových struktur je nutné vyřešit zarovnání hodnot a případnou existenci nebo neexistenci výplňových bajtů na konci takové struktury. V balíčku struct můžeme explicitně určovat, že se použijí výplňové bajty, s využitím specifikátoru „x“ (například „4ד značí čtyři výplňové bajty, ukládají se do nich nuly). To je sice užitečné, ale k dispozici máme ještě jednu možnost – specifikovat, že zarovnání a výplně se mají řešit přesně tak, jak to dělají céčkové překladače na dané platformě. Tím bude zajištěna možnost komunikace mezi Pythonem a céčkem. Tento režim je povolený v případě, že prvním znakem v řetězci s formátem je znak „@“, nebo se zde žádný specifikátor způsobu uložení nenachází. Navíc tento znak ovlivní i bitové (a tedy i bajtové) šířky numerických hodnot atd.
6. Zarovnání a výplně
Vyzkoušejme si nyní provést serializaci trojice hodnot s formáty „dvoubytové slovo+bajt+dvoubytové slovo“ do binárního bloku, přičemž nejprve použijeme výchozí způsob uložení, dále nativní způsob bez zarovnání a konečně explicitně nastavený nativní způsob se zarovnáním:
import struct bytes1 = struct.pack("hbh", 1, 2, 3) print(type(bytes1)) print(bytes1.hex(" ", 1)) print() bytes2 = struct.pack("=hbh", 1, 2, 3) print(type(bytes2)) print(bytes2.hex(" ", 1)) print() bytes3 = struct.pack("@hbh", 1, 2, 3) print(type(bytes3)) print(bytes3.hex(" ", 1))
Výsledky serializace budou odlišné, právě kvůli zarovnání u prvního a posledního formátu:
<class 'bytes'> 01 00 02 00 03 00 <class 'bytes'> 01 00 02 03 00 <class 'bytes'> 01 00 02 00 03 00
Otestujme podobný příklad, ovšem nyní s hodnotami odlišných typů. Pro větší čitelnost výsledků se zde ukládají hodnoty 1, 0×ffff (16bitů) a 3. První a poslední hodnota přitom může být uložena ve čtyřech bajtech (NEnativní serializace) nebo na některých platformách i v odlišném počtu bajtů (nativní serializace):
import struct bytes1 = struct.pack("iHi", 1, 0xffff, 3) print(type(bytes1)) print(bytes1.hex(" ", 1)) print() bytes2 = struct.pack("=iHi", 1, 0xffff, 3) print(type(bytes2)) print(bytes2.hex(" ", 1)) print() bytes3 = struct.pack("@iHi", 1, 0xffff, 3) print(type(bytes3)) print(bytes3.hex(" ", 1))
Na platformě x86(64) dostaneme tyto výsledky:
<class 'bytes'> 01 00 00 00 ff ff 00 00 03 00 00 00 <class 'bytes'> 01 00 00 00 ff ff 03 00 00 00 <class 'bytes'> 01 00 00 00 ff ff 00 00 03 00 00 00
U nativního způsobu zarovnání většinou platí, že kratší prvky (bajty, šestnáctibitové hodnoty) jsou zarovnány na stejnou šířku, jako delší prvky (32bitové a 64bitové hodnoty). Opět si to vyzkoušejme, nyní s krajními prvky typu „i“, což může odpovídat 32bitovým či 64bitovým hodnotám (v NEnativní serializaci jsou to 32bitové hodnoty):
import struct bytes1 = struct.pack("lHl", 1, 0xffff, 3) print(type(bytes1)) print(bytes1.hex(" ", 1)) print() bytes2 = struct.pack("=lHl", 1, 0xffff, 3) print(type(bytes2)) print(bytes2.hex(" ", 1)) print() bytes3 = struct.pack("@lHl", 1, 0xffff, 3) print(type(bytes3)) print(bytes3.hex(" ", 1))
Výsledky nyní mohou být odlišné v případě, že „i“ bude na dané platformě chápáno jako 64bitová hodnota nebo hodnota zarovnaná na 64bitů:
<class 'bytes'> 01 00 00 00 00 00 00 00 ff ff 00 00 00 00 00 00 03 00 00 00 00 00 00 00 <class 'bytes'> 01 00 00 00 ff ff 03 00 00 00 <class 'bytes'> 01 00 00 00 00 00 00 00 ff ff 00 00 00 00 00 00 03 00 00 00 00 00 00 00
7. Výpočet velikosti binární struktury se zadaným formátem prvků
V některých situacích, například při blokových přenosech dat, vytváření hlaviček chunků, alokaci paměti atd. je nutné vypočítat, jaká vlastně bude velikost binární struktury se serializovanými daty. Pochopitelně je možné si takovou strukturu vytvořit zavoláním struct.pack a následně získat její délku v bajtech, ovšem to nemusí být nejrychlejší ani paměťově nejefektivnější řešení. Ovšem samotná knihovna struct programátorům dává k dispozici pomocnou funkci nazvanou jednoduše calcsize (resp. struct.calcsize v závislosti na způsobu importu). Této funkci se předává pouze řetězec s popisem formátu použitého při serializaci nebo deserializaci binárních dat a vrací se velikost binárních dat v bajtech:
calcsize(format, /) Return size in bytes of the struct described by the format string.
8. Velikost binární struktury s výplněmi
Chování výše zmíněné funkce struct.calcsize si opět vyzkoušíme. Nejprve si vytvoříme pomocnou funkci, které se předá řetězec se specifikací formátu. Tato funkce tento řetězec vypíše a taktéž vypíše vypočtenou délku binárních dat (v bajtech) při serializaci struktury popsané formátem:
def size_for_format(format): size = struct.calcsize(format) print(f"{format:>6}:{size}")
Následně si necháme vypsat délky binárních bloků po serializaci struktury, a to například tímto způsobem:
size_for_format("b") size_for_format("iBi") size_for_format("=iBi") size_for_format("@iBi")
Úplný zdrojový kód tohoto demonstračního příkladu vypadá následovně:
import struct def size_for_format(format): size = struct.calcsize(format) print(f"{format:>6}:{size}") size_for_format("b") print() size_for_format("iBi") size_for_format("=iBi") size_for_format("@iBi") print() size_for_format("lBl") size_for_format("=lBl") size_for_format("@lBl") print() size_for_format("fBf") size_for_format("=fBf") size_for_format("@fBf") print() size_for_format("dBd") size_for_format("=dBd") size_for_format("@dBd") print() size_for_format("dBBd") size_for_format("=dBBd") size_for_format("@dBBd") print() size_for_format("dBBBd") size_for_format("=dBBBd") size_for_format("@dBBBd")
Ze zobrazených výsledků je patrné, jaký vliv má znak „=“ na vypočtenou velikost binární struktury a taktéž na jejím interním uspořádání. Tento znak totiž nastavuje „Pythonovský“ formát bez zarovnání a bez výplňových bajtů. Ten je kompatibilní napříč platformami, ovšem není kompatibilní například s céčkem:
b:1 iBi:12 =iBi:9 @iBi:12 lBl:24 =lBl:9 @lBl:24 fBf:12 =fBf:9 @fBf:12 dBd:24 =dBd:17 @dBd:24 dBBd:24 =dBBd:18 @dBBd:24 dBBBd:24 =dBBBd:19 @dBBBd:24
9. Doplňkové bajty na konci binární struktury
Existují datové formáty a přenosové protokoly, které vyžadují, aby celá binární datová struktura byla uložena takovým způsobem, že její délka bude násobkem (například) dvou bajtů, čtyř bajtů atd. To v praxi znamená, že na konci takového binárního bloku mají být uloženy výplňové bajty (padding). I binární struktury s takovým formátem můžeme při použití standardního balíčku struct realizovat, a to s využitím malého triku – ve formátovacím řetězci na jeho konci specifikujeme, že se má uložit nula prvků o velikosti dvoubajtového slova, čtyřbajtového slova atd.
Formátovací řetězec by tedy mohl končit znaky „0h“, „0i“, „0l“ atd. V takovém případě Python automaticky do binárních dat doplní n nulových bajtů. Kolik těchto bajtů bude, záleží na délce struktury – výsledná délka však bude dělitelná dvěma, čtyřmi, osmi atd. – podle nastavení.
import struct bytes1 = struct.pack("lB", 1, 0xff) print(type(bytes1)) print(bytes1.hex(" ", 1)) print() bytes2 = struct.pack("lB0h", 1, 0xff) print(type(bytes2)) print(bytes2.hex(" ", 1)) print() bytes3 = struct.pack("lB0i", 1, 0xff) print(type(bytes3)) print(bytes3.hex(" ", 1)) print() bytes4 = struct.pack("lB0l", 1, 0xff) print(type(bytes4)) print(bytes4.hex(" ", 1))
Povšimněte si výplňových bajtů doplněných do serializovaných dat. Tyto bajty jsou podtrženy:
<class 'bytes'> 01 00 00 00 00 00 00 00 ff <class 'bytes'> 01 00 00 00 00 00 00 00 ff 00 <class 'bytes'> 01 00 00 00 00 00 00 00 ff 00 00 00 <class 'bytes'> 01 00 00 00 00 00 00 00 ff 00 00 00 00 00 00 00
10. Krátké zopakování z minula: zápis rastrového obrázku do formátu PNG
Ve druhé části dnešního článku navážeme na demonstrační příklad uvedený minule. Připomeňme si, že se jednalo o skript, na jehož vstupu byly barvy pixelů rastrového obrázku (bitmapy). Skript tento obrázek uložil do formátu PNG, a to včetně korektní hlavičky, rastrových dat kódovaných s využitím algoritmu DEFLATE, korektně vypočtených kontrolních součtů atd. Využili jsme přitom dvojici standardních knihoven, konkrétně knihovny struct (tu si stále popisujeme) a taktéž knihovnu zlib. Celý zdrojový kód skriptu, který nejdříve vypočte barvy pixelů v bitmapě a následně tuto bitmapu uloží do PNG, vypadal následovně:
""" Zápis rastrového obrázku do formátu PNG.""" # Inspirace: # https://stackoverflow.com/questions/902761/saving-a-numpy-array-as-an-image import struct import zlib PNG_SIGNATURE = b'\x89PNG\r\n\x1a\n' FILTER_TYPE = b'\x00' def prepare_raw_data(buffer, width, height): """Konverze barev pixelů z bufferu do podoby se specifikací filtru na každém řádku.""" raw_data = bytearray() offset = 0 for _ in range(height): # nastavit filtr + zkopirovat jeden radek (scanline) raw_data += FILTER_TYPE + buffer[offset:offset+width*3] # na dalsi radek ve zdrojovem bufferu offset += width*3 return raw_data def png_chunk(png_tag, chunk_data): """Konstrukce jednoho PNG chunku s tagem i závěrečným kontrolním kódem.""" chunk_header = png_tag + chunk_data return (struct.pack("!I", len(chunk_data)) + chunk_header + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_header))) def write_png(buffer, width, height): """Uložení rastrového obrázku z bufferu do PNG.""" raw_data = prepare_raw_data(buffer, width, height) return b''.join([ PNG_SIGNATURE, png_chunk(b'IHDR', struct.pack("!2I5B", width, height, 8, 2, 0, 0, 0)), png_chunk(b'IDAT', zlib.compress(raw_data, level=9, wbits=15)), png_chunk(b'IEND', b'')]) WIDTH = 256 HEIGHT = 256 # buffer pro rastrová data pixels = bytearray(WIDTH*HEIGHT*3) # vybarvení testovacího obrázku index = 0 for i in range(HEIGHT): for j in range(WIDTH): pixels[index] = 0xff index+=1 pixels[index] = i index+=1 pixels[index] = j index+=1 data = write_png(pixels, WIDTH, HEIGHT) with open("test.png", 'wb') as fout: fout.write(data)
11. Formát PNG
Formát PNG je interně relativně jednoduchý. Začíná osmibajtovou signaturou s přesně specifikovanými hodnotami bajtů (nesmí se lišit) a poté následuje série takzvaných chunků, tj. bloků proměnné délky. Každý chunk se přitom skládá ze čtyř částí:
- První část má konstantní velikost čtyř bajtů a obsahuje celkovou délku datové části chunku. Teoreticky je tedy maximální délka datové části rovna 232-1 bytům; avšak pro snazší implementaci, například v těch programovacích jazycích, které nemají implementovaný beznaménkový datový typ (v té době se jednalo například o Javu), je maximální hodnota délky rovna „pouze“ 231-1 bytům. V praxi jsou však délky chunků pochopitelně o několik řádů menší.
- Druhá část chunku má opět velikost čtyři byty. Obsahuje jméno (typ) chunku ve formátu čtyř ASCII znaků malé i velké anglické abecedy. Jedná se o ASCII kódy v rozsahu 65–90 a 97–122 decimálně. Jedná se o velmi dobře navržený způsob pojmenování, protože jméno chunku může být načteno a následně rozpoznáno pouhou jednou operací porovnání (32 bitových integerů) bez nutnosti implementace řetězcového porovnání a současně je typ chunku velmi dobře čitelný i pro člověka. Velikost znaků ve jménu (minusky/verzálky) dále určuje, zda je daný chunk pro zpracování obrázku volitelný či povinný.
- Třetí část chunku je tvořena vlastními daty. Tato část může v některých případech mít nulovou délku.
- Poslední čtvrtá část chunku má délku čtyři byty a obsahuje kontrolní součet (CRC) druhé a třetí části, tj. jména (typu) chunku a uložených dat. Vzhledem k tomu, že délka chunku není do kontrolního součtu zahrnuta, je možné CRC generovat přímo při zápisu dat bez nutnosti druhého průchodu v případě, že délka chunku není dopředu známá (například při komprimaci). Použitý polynom má tvar:
x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1
12. Testovací obrázek, který budeme načítat a analyzovat
Pro otestování dále popsaného programového kódu určeného pro analýzu dat uložených ve formátu PNG byl vytvořen jednoduchý testovací obrázek dostupný na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/binary_structs/test.png, jenž má délku 132 bajtů. Tento obrázek má rozlišení 1×1 pixel a obsahuje barvovou paletu, informaci o průhledné barvě (transparency) a taktéž vložený informační text. Podívejme se na hexadecimální výpis obsahu tohoto souboru. Na pravé straně jsou zobrazeny tisknutelné znaky nalezené v souboru, z nichž je při podrobnějším zkoumání patrné, kde se nacházejí jednotlivé chunky nazvané IHDR, PLTE, tRNS, IDAT, iTXt a IEND:
00000000: 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 .PNG........IHDR 00000010: 00 00 00 01 00 00 00 01 08 03 00 00 00 28 cb 34 .............(.4 00000020: bb 00 00 00 03 50 4c 54 45 00 00 00 a7 7a 3d da .....PLTE....z=. 00000030: 00 00 00 01 74 52 4e 53 00 40 e6 d8 66 00 00 00 ....tRNS.@..f... 00000040: 0a 49 44 41 54 08 5b 63 60 00 00 00 02 00 01 62 .IDAT.[c`......b 00000050: 40 4f 68 00 00 00 19 69 54 58 74 6c 69 6e 6b 00 @Oh....iTXtlink. 00000060: 00 00 77 77 77 2e 72 6f 6f 74 2e 63 7a 00 2d 67 ..www.root.cz.-g 00000070: 00 31 2e 30 10 df f9 79 00 00 00 00 49 45 4e 44 .1.0...y....IEND 00000080: ae 42 60 82 .B`.
Právě z tohoto binárního souboru se budeme pokoušet číst jednotlivé údaje.
13. Čtení signatury souborů PNG
Celé čtení a interpretaci informací ze souboru, který obsahuje (nebo by alespoň měl obsahovat) rastrový obrázek uložený ve formátu PNG, pro jednoduchost provedeme v bloku with, ve kterém příslušný soubor otevřeme v režimu binárního čtení. Tím zajistíme, že znaky pro konce řádku atd. nebudou interpretovány tak, jakoby se jednalo o textový soubor (ano, ani po 45 letech od vydání první verze DOSu, jsme se tohoto problému nezbavili):
with open("test.png", "rb") as fin: # v tomto bloku budeme postupně číst jednotlivé části PNG
Na začátku souborů ve formátu PNG se musí nacházet takzvaná signatura, což je sekvence osmi bajtů s konstantními hodnotami:
89 50 4e 47 0d 0a 1a 0a
V Pythonu můžeme signaturu zapsat následujícím způsobem:
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
Signaturu načteme snadno:
# nacteni signatury signature = fin.read(len(PNG_SIGNATURE))
Následně zkontrolujeme její délku, tedy vlastně to, jestli soubor obsahuje alespoň osm bajtů. i obsah signatury:
# kontrola signatury assert len(signature) == len(PNG_SIGNATURE) assert signature == PNG_SIGNATURE
14. Načtení hlaviček jednotlivých chunků
Ihned za signaturou PNG následují jednotlivé chunky (začínat by se konkrétně mělo chunkem IHDR). Každý chunk začíná hlavičkou reprezentovanou jednoduchou strukturou:
Typ dat | Význam |
---|---|
4 bajty | délka chunku (network byte order) |
4 znaky | jméno chunku |
Příslušný formát tedy můžeme popsat řetězcem:
"!I4s"
Realizace programové smyčky, ve které se postupně načítají jednotlivé chunky, tedy může v první variantě vypadat následovně:
while True: # nacteni hlavicky chunku chunk_header = fin.read(8) if len(chunk_header) < 8: print(f"End of file with remaining {len(chunk_header)} bytes") break # hlavicka obsahuje delku (4 bajty) a ctyri znaky se jmenem s = struct.Struct("!I4s") length, png_tag = s.unpack(chunk_header) ... ... ...
End of file with remaining 0 bytes
Pokud se vypíše jiné číslo 1–7, je soubor nějakým způsobem poškozen.
15. Přeskok datové části chunku a přečtení kontrolního součtu
Za hlavičkou chunku následuje datová část, která má proměnlivou délku. Tuto část přeskočíme velmi snadno (bez čtení dat):
# preskocit data chunku fin.seek(length, 1)
V posledních čtyřech bajtech chunku je uložen kontrolní součet (CRC32). Tuto hodnotu načteme následovně:
# nacteni CRC32 chunku c = struct.Struct("!I") crc_block = fin.read(4) if len(crc_block) < 4: print("Error: not correct CRC block!") break crc = c.unpack(crc_block)[0]
Nyní již umíme přečíst celý obsah PNG a vypsat si základní informace o jednotlivých chuncích. Přitom se nesnažíme číst data, která nepotřebujeme (přeskakujeme je). Realizace může vypadat následovně:
while True: # nacteni hlavicky chunku chunk_header = fin.read(8) if len(chunk_header) < 8: print(f"End of file with remaining {len(chunk_header)} bytes") break # hlavicka obsahuje delku (4 bajty) a ctyri znaky se jmenem s = struct.Struct("!I4s") length, png_tag = s.unpack(chunk_header) # preskocit data chunku fin.seek(length, 1) # nacteni CRC32 chunku c = struct.Struct("!I") crc_block = fin.read(4) if len(crc_block) < 4: print("Error: not correct CRC block!") break crc = c.unpack(crc_block)[0] print(f"{png_tag.decode("ASCII")} {length:5} {crc:04x}")
16. Úplný zdrojový kód příkladu, který přečte chunky uložené v souboru formátu PNG
Podívejme se nyní na úplný zdrojový kód příkladu, po jehož spuštění se postupně načte a analyzuje soubor „test.png“, který obsahuje obrázek ve formátu PNG. Skript postupně vypíše všechny chunky, které se v souboru nachází, jejich velikost i kontrolní součet:
"""Informace o obrázku uloženého ve formátu PNG.""" import struct PNG_SIGNATURE = b'\x89PNG\r\n\x1a\n' with open("test.png", "rb") as fin: # nacteni signatury signature = fin.read(len(PNG_SIGNATURE)) # kontrola signatury assert len(signature) == len(PNG_SIGNATURE) assert signature == PNG_SIGNATURE # postupne nacteni jednotlivych chunku print("Chunk Length CRC") while True: # nacteni hlavicky chunku chunk_header = fin.read(8) if len(chunk_header) < 8: print(f"End of file with remaining {len(chunk_header)} bytes") break # hlavicka obsahuje delku (4 bajty) a ctyri znaky se jmenem s = struct.Struct("!I4s") length, png_tag = s.unpack(chunk_header) # preskocit data chunku fin.seek(length, 1) # nacteni CRC32 chunku c = struct.Struct("!I") crc_block = fin.read(4) if len(crc_block) < 4: print("Error: not correct CRC block!") break crc = c.unpack(crc_block)[0] print(f"{png_tag.decode("ASCII")} {length:5} {crc:04x}")
Zkusme si nyní tento skript spustit, přičemž se v aktuálním adresáři nachází i soubor „test.png“. Měli bychom získat tento výstup:
Chunk Length CRC IHDR 13 28cb34bb PLTE 3 a77a3dda tRNS 1 40e6d866 IDAT 10 62404f68 iTXt 25 10dff979 IEND 0 ae426082 End of file with remaining 0 bytes
17. Načtení informací uložených v chunku IHDR
Pro zajímavost se podívejme na způsob přečtení informací, které jsou uloženy v hlavičce, tedy konkrétně v chunku nazvaném IHDR. Tento chunk má konstantní délku a nepoužívá se zde žádné zarovnání ani doplnění bajtů, tj. ani alignment ani padding. Vícebajtové prvky používají network byte order, tj. big endian. Datová část chunku IHDR obsahuje následující položky:
Offset | Počet byte | Význam |
---|---|---|
00 | 4 | šířka obrázku uvedená v pixelech |
04 | 4 | výška obrázku uvedená v pixelech |
08 | 1 | bitová hloubka pixelů v barvovém kanálu (povolené hodnoty jsou 1, 2, 4, 8 a 16) |
09 | 1 | typ kódování barev (viz další tabulka) |
0a | 1 | použitá metoda komprimace (musí zde být 0-deflate) |
0b | 1 | použitá metoda filtrace (musí zde být 0-adaptivní filtrace) |
0c | 1 | prokládání obrázku (0-bez prokládání, 1-prokládání) |
Jak by tedy vypadal řetězec s popisem formátu této binární struktury? Můžeme použít delší zápis:
"!IIBBB"
nebo kratší zápis se specifikací opakování prvků stejného typu:
"!2I5B"
Do programové smyčky se čtením chunků můžeme zařadit větev, která přečte informace z hlavičky:
if png_tag == b"IHDR": # hlavicku nacist celou - ocekava se tento format: # Width: 4 bytes # Height: 4 bytes # Bit depth: 1 byte # Color type: 1 byte # Compression method: 1 byte # Filter method: 1 byte # Interlace method: 1 byte h = struct.Struct("!2I5B") chunk_data = fin.read(length) width, height, bit_depth, color_type, compression, filter, interlace = ( h.unpack(chunk_data) ) else: # preskocit data chunku fin.seek(length, 1)
Na konci skriptu informace přečtené z hlavičky vypíšeme. Pro výpis informací o barvovém prostoru použijeme pomocnou mapu color_type_desc. Formát PNG totiž povoluje skutečně pouze hodnoty 0, 2, 3, 4 a 6:
Typ kódování barev | Bitová hloubka | Popis |
---|---|---|
0 | 1,2,4,8,16 | Obrázek ve stupních šedi (popř. černobílá pérovka) |
2 | 8,16 | Plnobarevný (true color) obrázek typu RGB |
3 | 1,2,4,8 | Obrázek s barvovou paletou (v souboru musí existovat chunk PLTE) |
4 | 8,16 | Obrázek ve stupních šedi a průhledností (alfa kanálem) – nepříliš používaná kombinace |
6 | 8,16 | Obrázek, kde každý pixel obsahuje všechny čtyři hodnoty RGBA (tj. kromě tří barvových složek i alfa kanál) |
Realizace výpisu informací z hlavičky PNG:
color_type_desc = [ "grayscale", "unknown", "RGB", "color palette", "grayscale+alpha", "unknown", "RGBA", ] print(f"Resolution: {width}x{height}") print(f"Bit depth: {bit_depth} bpp") print(f"Color type: {color_type} = {color_type_desc[color_type]}") print(f"Compression: {compression}") print(f"Filter type: {filter}") print(f"Interlace: {interlace}")
18. Úplný zdrojový kód skriptu, který vypíše strukturu PNG souboru i přesné informace z hlavičky
V samotném závěru dnešního článku je ukázán úplný zdrojový kód skriptu, který po svém spuštění vypíše nejenom všechny chunky uložené ve formátu PNG, ale i obsah hlavičky, tj. dat v chunku IHDR:
"""Informace o obrázku uloženého ve formátu PNG.""" import struct PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" with open("test.png", "rb") as fin: # nacteni signatury signature = fin.read(len(PNG_SIGNATURE)) # kontrola signatury assert len(signature) == len(PNG_SIGNATURE) assert signature == PNG_SIGNATURE # postupne nacteni jednotlivych chunku print("Chunk Length CRC") while True: # nacteni hlavicky chunku chunk_header = fin.read(8) if len(chunk_header) < 8: print(f"End of file with remaining {len(chunk_header)} bytes") break # hlavicka obsahuje delku (4 bajty) a ctyri znaky se jmenem s = struct.Struct("!I4s") length, png_tag = s.unpack(chunk_header) if png_tag == b"IHDR": # hlavicku nacist celou - ocekava se tento format: # Width: 4 bytes # Height: 4 bytes # Bit depth: 1 byte # Color type: 1 byte # Compression method: 1 byte # Filter method: 1 byte # Interlace method: 1 byte h = struct.Struct("!2I5B") chunk_data = fin.read(length) width, height, bit_depth, color_type, compression, filter, interlace = ( h.unpack(chunk_data) ) else: # preskocit data chunku fin.seek(length, 1) # nacteni CRC32 chunku c = struct.Struct("!I") crc_block = fin.read(4) if len(crc_block) < 4: print("Error: not correct CRC block!") break crc = c.unpack(crc_block)[0] print(f"{png_tag.decode("ASCII")} {length:5} {crc:04x}") color_type_desc = [ "grayscale", "unknown", "RGB", "color palette", "grayscale+alpha", "RGBA", ] print(f"Resolution: {width}x{height}") print(f"Bit depth: {bit_depth} bpp") print(f"Color type: {color_type} = {color_type_desc[color_type]}") print(f"Compression: {compression}") print(f"Filter type: {filter}") print(f"Interlace: {interlace}")
Výsledky, které získáme po spuštění tohoto skriptu, pokud se použije obrázek „test.png“ z repositáře:
Chunk Length CRC IHDR 13 28cb34bb PLTE 3 a77a3dda tRNS 1 40e6d866 IDAT 10 62404f68 iTXt 25 10dff979 IEND 0 ae426082 End of file with remaining 0 bytes Resolution: 1x1 Bit depth: 8 bpp Color type: 3 = color palette Compression: 0 Filter type: 0 Interlace: 0
19. Repositář s demonstračními příklady
Všechny demonstrační příklady využívající standardní knihovnu struct lze nalézt v repositáři https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady:
20. Odkazy na Internetu
- Python standard types: bytes
https://docs.python.org/3.8/library/stdtypes.html#bytes - Python standard types: bytearray
https://docs.python.org/3.8/library/stdtypes.html#bytearray-objects - Bytes and Bytearray Operations
https://docs.python.org/3.8/library/stdtypes.html#bytes-methods - Standard encodings
https://docs.python.org/3.8/library/codecs.html#standard-encodings - class memoryview
https://docs.python.org/3.8/library/stdtypes.html#memoryview - struct – Interpret bytes as packed binary data
https://docs.python.org/3/library/struct.html - C-like structures in Python
https://stackoverflow.com/questions/35988/c-like-structures-in-python - python3: bytes vs bytearray, and converting to and from strings
https://stackoverflow.com/questions/62903377/python3-bytes-vs-bytearray-and-converting-to-and-from-strings - Základní informace o MessagePacku
https://msgpack.org/ - Balíček msgpack na PyPi
https://pypi.org/project/msgpack/ - MessagePack na Wikipedii
https://en.wikipedia.org/wiki/MessagePack - Comparison of data-serialization formats (Wikipedia)
https://en.wikipedia.org/wiki/Comparison_of_data-serialization_formats - Repositáře msgpacku
https://github.com/msgpack - Specifikace ukládání různých typů dat
https://github.com/msgpack/msgpack/blob/master/spec.md - Podpora MessagePacku v různých programovacích jazycích
https://msgpack.org/#languages - Základní implementace formátu msgpack pro programovací jazyk Go
https://github.com/msgpack/msgpack-go - go-codec
https://github.com/ugorji/go - Gobs of data (odlišný serializační formát)
https://blog.golang.org/gobs-of-data - Formát BSON (odlišný serializační formát)
http://bsonspec.org/ - Problematika nulových hodnot v Go, aneb proč nil != nil
https://www.root.cz/clanky/problematika-nulovych-hodnot-v-go-aneb-proc-nil-nil/ - IEEE-754 Floating Point Converter
https://www.h-schmidt.net/FloatConverter/IEEE754.html - Base Convert: IEEE 754 Floating Point
https://baseconvert.com/ieee-754-floating-point - Brain Floating Point – nový formát uložení čísel pro strojové učení a chytrá čidla
https://www.root.cz/clanky/brain-floating-point-ndash-novy-format-ulozeni-cisel-pro-strojove-uceni-a-chytra-cidla/ - Marshalling (computer science)
https://en.wikipedia.org/wiki/Marshalling_(computer_science) - Protocol Buffers
https://protobuf.dev/ - Protocol Buffers
https://en.wikipedia.org/wiki/Protocol_Buffers - What is the difference between Serialization and Marshaling?
https://stackoverflow.com/questions/770474/what-is-the-difference-between-serialization-and-marshaling - Comparison of data-serialization formats
https://en.wikipedia.org/wiki/Comparison_of_data-serialization_formats - PNG (Portable Network Graphics) Specification, Version 1.2
http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html - Data structure alignment
https://en.wikipedia.org/wiki/Data_structure_alignment - Byte alignment and ordering
https://www.eventhelix.com/embedded/byte-alignment-and-ordering/ - The Lost Art of Structure Packing
http://www.catb.org/esr/structure-packing/ - Padding is hard
https://dave.cheney.net/2015/10/09/padding-is-hard - Structure Member Alignment, Padding and Data Packing
https://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/ - C Alignment Cheatsheet
https://github.com/Q1CHENL/c-alignment-cheatsheet - Struct padding rules in Rust
https://stackoverflow.com/questions/70587534/struct-padding-rules-in-rust - Deflate
https://en.wikipedia.org/wiki/Deflate