elwin013's blog

kawałek sieci młodego geeka

Cześć, jestem Kamil! Witaj na moim blogu!

Kodowanie znaków w UTF-8 i UTF-16

Studia na kierunku informatyka na WAT obfitują w różnego rodzaju moduły (przedmioty). Jednym z nich jest Teoria Informacji i Kodowania (TIK) i to właśnie na nich zajmowaliśmy się ostatnio tematem wymienionym w tytule tego wpisu i wbrew pozorom jest to całkiem ciekawe – chociaż zależy co kto uważa za „ciekawe”. ;-)

Czym jest Unicode zapewne prawie każdy wie. Jeśli jednak jest inaczej to pomocą zawsze służy Wikipedia:

Unicode – komputerowy zestaw znaków mający w zamierzeniu obejmować wszystkie pisma używane na świecie.
Źródło: Unicode – Wikipedia, wolna encyklopedia

Nie jest to jednak sposób zapisu (kodowania znaków) sam w sobie. Jest kilka sposobów kodowania, oznaczanych literami UTF (Unicode Transformation Format) oraz UCS (Universal Character Set). Spośród nich chciałbym się jednak w dzisiejszym wpisie skupić na kodowaniu UTF-8 i UTF-16. Chciałbym jednak zwrócić uwagę, że w poniższych rozważaniach będę korzystał przede wszystkim z heksadecymalnego (szesnastkowego) zapisu liczb.

UTF-16

W przypadku UTF-16 w większości przypadków sprawa kodowania jest prosta – dla zakresu znaków Unicode U+0000 – U+FFFF (z wyłączeniem zakresu U+D800-U+DFFF, o którym za chwilę) kod jest identyczny do punktu kodowego. Czyli na przykład znak „Ń”, którego punkt kodowy to U+0143 w UTF-16 zostanie przedstawiony po prostu jako 01 43. W tym też zakresie, zwanym Basic Multilingual Plane (BMP) zawarte są najczęściej używane znaki.

Co jednak gdy potrzebujemy zapisać jakiś znak, którego punkt kodowy wykracza ponad U+FFFF? W tym momencie do działania wchodzi pominięty wcześniej zakres U+D800-U+DFFF. Weźmy na warsztat znak o punkcie kodowym U+54321 (czyli liczba 0x54321). Pierwsze co musimy zrobić to odjąć od niej liczbę 0x10000.

   0101 0100 0011 0010 0001 = 0x54321
 - 0001 0000 0000 0000 0000 = 0x10000
---------------------------
   0100 0100 0011 0010 0001 = 0x44321

W wyniku otrzymujemy liczbę, którą możemy zapisać na dwudziestu bitach. Następnie do bardziej znaczących 10 bitów (czyli 10 bitów od lewej strony, w liczbie zapisanej na dwudziestu bitach) dodajemy 0xD800.

   1101 1000 0000 0000 = 0xD800
 +        01 0001 0000 = 0x0110
----------------------
   1101 1001 0001 0000 = 0xD910

W ten sposób otrzymujemy pierwszy fragment naszej liczby. Natomiast do mniej znaczących 10 bitów (czyli 10 licząc od prawej) dodajemy liczbę 0xDC00.

   1101 1100 0000 0000 = 0xDC00
 +        11 0010 0001 = 0x0321
----------------------
   1101 1111 0010 0001 = 0xDF21

W ten sposób otrzymujemy drugi fragment naszej liczby. Czyli znak zawarty pod punktem kodowym U+54321 zostanie zakodowany w UTF-16 w postaci 4 bajtów D910 DF21 (szesnastkowo).

Jest to zapis UTF-16 zachowując kolejność bajtów Big Endian (najbardziej znaczący bajt jako pierwszy). W przypadku zapisu np. do pliku jest ona poprzedzana tzw. BOM – Byte Order Mark (znacznik kolejności bajtów), którym jest FE FF.

Możemy także zapisać UTF-16 zachowując kolejność bajtów Little Endian (najmniej znaczący bajt jako pierwszy). W tym przypadku plik jest poprzedzany kombinacją FF FE, a nasza liczba wygląda następująco – 10D9 21DF (pary bajtów są zamienione miejscami).

UTF-8

Trochę inaczej sprawa wygląda w przypadku drugiego kodowania czyli UTF-8 – tylko w przypadku pierwszych 127 znaków (tożsamych z tymi zawartymi w ASCII) jest trywialny. Wtedy kod jest identyczny z punktem kodowym – na przykład cyfra „1” to 0x31, a znak „Z” – 0x5A. W przypadku gdy znak (a raczej jego punkt kodowy) wykracza poza ten zakres musimy do zakodowania użyć większej ilości bajtów – 2, 3 lub 4. Sposób ten można potocznie nazwać „pakowaniem w wagoniki”. ;-)

Pierwszy „wagonik” poza przechowywaniem części naszego punktu kodowego wskazuje także na ilość wagoników – jest ich dokładnie tyle, ile znajduje się jedynek przed oddzielającym zerem. Czyli na przykład 110xxxxx wskazuje na dwa wagoniki – czyli dodatkowo jeszcze jeden prócz tego wymienionego. Każdy wagonik zawiera na początku sekwencje dwóch bitów 10, dopiero po której następują bity naszego znaku. Podsumowując, kolejne sekwencje będą wyglądać następująco:

0xxxxxxx                            -  7 bitów
110xxxxx 10xxxxxx                   - 11 bitów
1110xxxx 10xxxxxx 10xxxxxx          - 16 bitów
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - 21 bitów

Prześledźmy jednak to wszystko na przykładzie – weźmy na warsztat znak „Ń” o punkcie kodowym U+0143.
Pierwsze co należy zrobić to przeliczyć wartość punktu kodowego na system binarny: 0x0143 = 0000 0001 0100 0011 – widzimy, że liczba 0x0143 mieści się na 9 bitach, potrzebujemy ich więc co najmniej tyle w wagonikach. Jak widać wyżej przy pomocy dwóch możemy zapisać 11 bitów – zróbmy więc to, uzupełniając od prawej do lewej, w razie potrzeby dopisując zera:

Wpisujemy od prawej do lewej:
110xx101 10000011
Uzupełniamy wolne miejsca zerami:
11000101 10000011

Czyli nasz znak „Ń” zostanie zakodowany w UTF-8 jako C5 83.

Ponadto warto zwrócić uwagę na zapis znaków zakodowanych w UTF-8 do pliku. Zwykle (choć nie muszą) są one poprzedzane znacznikiem EF BB BF, który wskazuje jakie kodowanie używane jest w pliku. Jest on również określany mianem BOM, jednak nie wskazuje kolejności bajtów – zapis w UTF-8 jest jednoznaczny.

Dla chcących poćwiczyć przygotowałem prostą tabelę zawierającą polskie znaki – „UTF-8 – polskie ogonki”. Możesz w niej sprawdzić, lub na stronie www.utf8-chartable.de na której bazowałem, że znakowi „Ń” naprawdę odpowiada punkt kodowy U+0143 oraz kod C5 83 w UTF-8.

Jak widać powyżej nie taki diabeł straszny jak go malują, a samo kodowanie znaków jest całkiem ciekawe i niekoniecznie skomplikowane. I na tym wpis należało by zakończyć – jeśli jednak gdzieś się pomyliłem, naginam fakty, czy coś jest niezrozumiałe – napisz w komentarzu, postaram się wyjaśnić wątpliwości. Do następnego! :-)


4 komentarze do “Kodowanie znaków w UTF-8 i UTF-16”

Bardzo przydatny artykuł! Dziękuję za jego udostępnienie i poświęcony czas. Pozdrawiam!

Zostaw komentarz