Unicode, UTF-8, UTF-16, UTF-32, BOM – porównanie i cechy poszczególnych sposobów kodowania znaków

Unicode i UTF to ściśle powiązane ze sobą pojęcia. Dotyczą sposobów kodowania znaków pisma ludzkiego do elektronicznej formy. Mimo, że nie jest konieczna dogłębna znajomość poszczególnych sposobów kodowania, to już mylenie Unicode i UTF może być dość niepraktyczna. Ale zacznijmy od początku…

Pierwszą kwestią jest Unicode. Wbrew temu, co niektórym może się na początku wydawać, Unicode to jeszcze nie kodowanie. Jest to standard, który przypisuje wszystkim znakom pisma (i nie tylko) wartość liczbową. Unicode jest podzielony na kilka przedziałów zwanych z ang. planes. Poniżej mały fragment tablicy Unicode, w którym pokazana jest polska litera ą, która ma przypisaną wartość liczbową 0×105.

Unicode Table Fragment

Skoro już mamy wszystkim znakom na świecie przypisaną wartość liczbową, to trzeba to jakoś zapisać za pomocą bitów. I tu pojawia się kodowanie UTF. UTF-8, UTF-16 i UTF-32 to trzy różne sposoby zapisania wartości liczbowej z tablicy Unicode do postaci bitowej. Zacznijmy od UTF-32, gdyż jest to najprostsze spośród tych trzech kodowań.

UTF-32

UTF-32 w odróżnieniu od UTF-16 i UTF-8 ma stałą długość każdego znaku wynoszącą 32 bity (tj. 4 bajty). Wadą takiego rozwiązania jest to, że marnuje to bardzo dużo miejsca. Wynika to z tego, że 4 bajty mogą przyjąć wartości od 0×00000000 do 0xFFFFFFFF, natomiast wszystkie znaki europejskich języków mieszczą się w przedziale 0×0000 – 0xFFFF. Innymi słowy, kodując w UTF-32 prawie zawsze przynajmniej połowa bitów jest zerowa. Ma to też swoje zalety. Stała długość znaków pozwala na dokładne odnalezienie N-tego znaku w tekście, bez konieczności przebrnięcia przez cały poprzedzający tekst.

Endianess

W tym miejscu nadmienię przy okazji pewną kwestię związaną ze sposobem zapisu bitów. Weźmy np literę ‘ą’, która w UTF-32 ma postać: 0×00000105. Poszczególne bajty w pamięci (na pliku) możemy zapisać właśnie tak: 00 00 01 05. Ale możemy również zapisać odwrotnie: 05 01 00 00. Obydwa sposoby są stosowane. Pierwszy nazywa się Big-Endian (BE), a drugi nazwy się Little-Endian (LE).

Standardowo stosowany jest zapisy Big-Endian. Istnieje jednak sposób, aby wprost określić stosowanie BE lub LE. Ten sam sposób jest często używany, żeby w ogóle zaznaczyć, że tekst jest zakodowany w UTF. Tym sposobem jest BOM (Byte Order Mark). Jest to specjalny ciąg znaków podawany na samym początku tekst. Poniższa tabelka pokazuje różne postaci BOM’a.

BOM Formats

UTF-8

Kodowanie UTF-8 jest bardzo odmienne od UTF-32. Znaki nie mają stałej długości. Mogą zawierać od 1 do 6 bajtów. Pierwsze znaki Unicode są kodowane jednym bajtem i tym samym są identyczne z kodem ASCII. Kolejne znaki są odpowiednio kodowane 2, 3, 4, 5 i 6 bajtami. Zasada kodowania w UTF-8 jest taka. Pierwsze bity, pierwszego bajtu określają precyzyjnie z ilu bajtów składa się znak. Kolejne bajty mają za to stały 2-bitowy prefiks. Pokazuje to poniższa tabelka.

UTF8 Encoding

Kiedy chcemy rozkodować taki znak Unicode to bierzemy bity stojące w miejscach X-sów i łączymy je razem. Weźmy przykład naszej litery ‘ą’. Jej wartość w tabeli Unicode to 0×105. Z tabelki widać, że potrzebujemy do jej zapisania w UTF-16 dwóch bajtów. Pokazuje to przykład poniżej:

1. Najpierw mamy literkę ‘ą’ zapisaną szesnastkowo

0X105

2. Rozbijamy ten zapisy na dwa bajty zapisane szesnastkowo

0x01 0x05

3. Zapisujemy te dwa bajty binarnie

0000 0001    0000 0101

4. Obcinamy ten zapis do 11 bitów (co pokazuje powyższa tabela)

____ _001    0000 0101

5. Poszczególne bity umieszczamy w miejsce X-sów

   0 0100      00 0101
110X XXXX    10XX XXXX
1100 0100    1000 0101

Teraz 3 ważne pytania, które ktoś może zadać. Pierwsze to, dlaczego tabelka pokazuje, że ostatnim znakiem Unicode, który można zakodować w UTF-8 jest U+7FFFFFFF? Odpowiedź jest prosta. To i tak więcej niż potrzeba. Wszystkie, wszystkie znaki Unicode mieszczą się w zakresie 0×000000 – 0x10FFFF.

Drugie pytanie to, dlaczego nieco wyższa tabela mówi, że w kodowaniu UTF-8 nie ma rozróżnienia w BOM’ie dla Little Endian i Big Endian. To dlatego, że kodowanie UTF-8 jest zorientowane bajtowo. Oznacza to, że (na bardzo niskim poziomie abstrakcji) czytamy za każdym razem tylko jeden bajt i wtedy dopiero decydujemy co daje na podstawie tego, jaką wartość ma ten bajt.

Trzecie pytanie to, czy budowa kodowania UTF-8 nie pozwala przypadkowo zapisać tego samego znaku na kilka różnych sposobów. Odpowiedź brzmi – tak, pozwala, ale standard mówi, że poprawnym zapisem jest tylko najkrótszy z możliwych.

UTF-16

Istnieje jeszcze kodowanie UTF-16. Zanim przejdziemy do jego omawiania, należy zapamiętać, że istnieje pewien zakres znaków Unicode, który nie jest przypisany do żadnych znaków. Niestosowanie tego zakresu wymusza na nas również powyższa tabela (co widać przy głębszym jej zbadaniu). Jest to zakres od 0xD800 do 0xDFFF. Zakaz stosowania bajtów o takich wartościach wynika z tego, że bajty właśnie z tego zakresu służ do kodowania w UTF-16.

UTF-16 wykazuje pewne podobieństwo zarówno do UTF-8 oraz do UTF-32. Pierwsze podobieństwo to takie, że znaki w UTF-16 mogą mieć tylko albo 2 bajty, albo 4 bajty. Pierwsza część symboli Unicode jest kodowana właśnie przy użyciu tylko 2 bajtów, a dalsze symbole kodowane są przy użyciu 4 bajtów. UTF-16 ma więc zmienną długość symbolu, ale nie tak bardzo jak UTF-8.

Zasada kodowania jest taka, że symbole Unicode z przedziału U+0000 do U+FFFF (wyłączając opisany powyżej zabroniony przedział będzie to U+0000 do U+D7FF oraz U+E000 do U+FFFF) czyli najczęściej stosowane znaki Unicode zapisujemy numerycznie zgodnie z wartością w tabeli Unicode. Innymi słowy bierzemy wartość z tablicy Unicode i zapisujemy tak jak tam jest.

Symbole Unicode powyżej tego przedziału (czyli U+10000 do U+10FFFF) zapisujemy właśnie stosując zakazany przedział bajtów. Algorytm wygląda następująco:

1. Odejmujemy od wartości symbolu liczbę 0×10000. Zostaje nam wtedy zawsze 20-bitowa liczba z przedziału 0×0000 do 0xFFFFF.

2. Górne 10 bitów (tworzące liczbę z przedziału 0×000 do 0x3FF) dodajemy do liczby 0xD800. Otrzymujemy tym sposobem liczbę z przedziału 0xD800 – 0xDBFF zwaną lead surrogate.

3. Dolne 10 bitów (również tworzące liczbę z przedziału 0×000 do 0x3FF) dodajemy do liczby0xDC00. Otrzymujemy tym sposobem liczbę z przedziału 0xDC00 – 0xDFFF zwaną trail surrogate.

Taki sposób kodowania zapewni nam, że nigdy nie pomylimy lead surrogate z trail surrogate. Zakazany przedział Unicode zapewnia również to, że nigdy nie pomylimy surrogate z żadnym innym znakiem. Poniżej również zamieszczam ilustraję pomagającą w wyobrażeniu sobie, jakie zakresy znaków mozęmy zakodować za pomocą surrogates.

UTF16 Decoder

Wszystkie ilustracje i tabele pochodzą z Wikipedii.