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”
Znowu nie ma pozdrowień dla mnie :(
No niestety tak jakoś wyszło :(
Bardzo przydatny artykuł! Dziękuję za jego udostępnienie i poświęcony czas. Pozdrawiam!
Pozdro z TliK w 2022 na WAT
Zostaw komentarz