Kódování UTF-8 je obvykle považováno za zvláštní případ, odlišný od ostatních znakových sad. Co je na něm tak zajímavého a jak správně zacházet s textem v tomto formátu?

Jak jsem psal již dříve, pohnutek ke vzniku UTF-8 bylo několik. Toto kódování svými vlastnostmi řeší mnoho dřívějších problémů při zachování zpětné kompatibility. Nejdříve si tedy povězme, jakým způsobem se v něm vlastně zapisuje text.

Specifikace formátu UTF-8

Podrobná specifikace je v RFC 2279. Na tomto místě si objasníme alespoň základní principy zápisu textů v tomto formátu. V UTF-8 se zapisují Unicode kódy daných znaků, takže lze zapsat prakticky všechny znaky všech národních abeced na této planetě. Základní ASCII sada (tedy znaky s kódem do 127 včetně) se zapisuje způsobem shodným s osmibitovým kódováním, tedy co znak, to bajt. Ostatní znaky se však zapisují způsobem odlišným, dle následující tabulky:

Unicode kód od – do Binární zápis znaku v UTF-8
0000 0000 – 0000 007F 0xxxxxxx
0000 0080 – 0000 07FF 110xxxxx 10xxxxxx
0000 0800 – 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 – 001F FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
0020 0000 – 03FF FFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
0400 0000 – 7FFF FFFF 1111110x 10xxxxxx … 10xxxxxx

Jak vidíte, k dispozici máme 31 bitů, lze tedy zapsat 231 = přes dvě miliardy různých znaků. Každý z nich se přitom zakóduje do jednoho až šesti bajtů. Možná to tak na první pohled nevypadá, ale tento zápis má velice důležité vlastnosti, které tomuto kódování pomohly k současnému rozšíření.

Rysy UTF-8

Úkolem vývojářů bylo vytvořit takové kódování, které by bylo možné využít již v produktech, které podporují pouze osmibitové kódování, aby v něm bylo možno zapsat všechny Unicode znaky, a přitom aby docházelo k minimálnímu množství problémů.

Výhody

Zpětná kompatibilita
Běžné ASCII znaky lze do UTF-8 přímo zkopírovat, protože pro ně je zápis totožný. Naopak v UTF-8 mají všechny bajty náležící nějakému národnímu znaku kód větší než 127, takže nemohou být považovány za ASCII znak ani v prostředí, které zná pouze osmibitové kódování. To je důležité, neboť díky tomu lze UTF-8 využít i tam, kde se s ním nepočítalo. Například pokud bude zapsán ve zdrojovém kódu programu řetězcový literál pomocí UTF-8, nemůže si pak kompilátor splést nějaký národní znak třeba s uvozovkami, které by znamenaly konec literálu. To je zásadní odlišností od jiných zápisů Unicode (UCS-2, UCS-4), které nelze v osmibitovém prostředí vůbec využít.
Celistvost jednotlivých znaků
Lze snadno rozlišit, zda libovolný bajt je prvním v zápisu daného znaku, nebo některým dalším. K čemu je to dobré? Může se stát, že řetězec bude nějakou chybou useknut právě v místě mezi bajty jednoho znaku. Jak potom dekodér pozná, o jaký znak šlo, a kde začíná další? To díky použitému formátu není problém. Dekodér podle prvního nejvyššího bitu pozná, že jde o národní znak, a pokud je v druhém bitu nula, pak je zřejmé, že nejde o první bajt znaku. Pak stačí procházet řetězec bajt po bajtu, dokud nenarazí na začátek dalšího znaku. První znak je poškozený, nelze jej tedy použít. Dalším důsledkem je například možnost prohledávání textu nějakým starším osmibitovým algoritmem, který si nemůže poplést bajt uprostřed znaku s počátečním bajtem.
Snadná konverze do UCS-2 a zpět
Velká většina moderních platforem již dlouho pro vnitřní reprezentaci textů využívá UCS-2 nebo dokonce UCS-4. Důvody jsou zřejmé (velice efektivní manipulace s textem). Převod mezi nimi a UTF-8 je pouze záležitostí jednoduché binární aritmetiky, není třeba žádné převodní tabulky či dalšího zpracování.

Nevýhody

Abychom pouze nechválili, uvedeme si také některé nevýhody UTF-8. Většina z nich má původ již ve vlastnostech Unicode.

Obtížná manipulace s textem
Pokud chceme sestavit algoritmus, který má konvertovat velikost písmen (a/A) nebo například odstraňovat z textu diakritiku, zjistíme, že neexistuje snadný způsob, jak potřebnou konverzi provést. Musíme využít převodních tabulek, které jsou k dispozici v Unicode Character Database. Tyto tabulky nejsou zrovna malé a jejich použití může být občas poměrně těžkopádné.
Obtížné zjištění počtu znaků
Ve většině kódování (osmibitové, UCS-2, UCS-4) je každý znak vyjádřen určitým počtem bajtů (1, 2 nebo 4). Zjištění počtu znaků je pak snadno odvoditelné od binární délky textu. To však neplatí pro UTF-8, kde každý znak může mít jinou délku. Proto je nutné při zjišťování počtu znaků text procházet a počítat je jeden po druhém. Naštěstí tento úkol není příliš častý, většinou je třeba znát právě délku binární reprezentace (pro kontrolu, zda se text vejde do databáze, do omezení přenosového protokolu a podobně).
Příliš mnoho znaků je i nevýhodou
Vývojář HW/SW platformy, která se musí starat také o vykreslování znaků, musí počítat s tím, že Unicode definuje opravdu obrovské množství znaků, a každý z nich má jinou grafickou reprezentaci. Pro dobrou implementaci UTF-8 by měl zařídit správné zobrazení většiny. A teď si představte, že musíte vytvářet font obsahující 40 000 znaků.

Jak konvertovat text v UTF-8?

V předchozím článku jsme probírali možnosti převodu textu mezi osmibitovými znakovými sadami. Přitom jsme vždy získali Unicode kód daného znaku a pak jsme nalezli odpovídající znak v cílové znakové sadě. Z tohoto postupu budeme vycházet, protože pro nás je důležitý právě onen Unicode kód znaku.

Konverze do UTF-8

Převádíme-li znak z osmibitové znakové sady, provedeme první fázi podle nám již dobře známého postupu, tedy zjistíme Unicode kód znaku. Pak už stačí jen trochu binární matematiky, abychom vygenerovali potřebnou sekvenci bajtů. Nejprve zjistíme, do jakého rozsahu spadá kód převáděného znaku, a pak jej rozsekáme po jednotlivých skupinkách bitů, které „slepíme“ dle výše uvedené tabulky.

Stejně jako v předchozím článku, i zde jsou příklady v JavaScriptu, protože jeho syntaxe je velice podobná všeobecně známému jazyku C a každý si je může ihned vyzkoušet přímo v prohlížeči.

function convertTOutf8 (text) {
  var code, buff = „“;
  for(var i = 0; i<text.length; i++) {
    code = text.charCodeAt(i); // zde získáváme Unicode!
    if (code < 0x00000080)
      buff += String.fromCharCode(code) // standardní ASCII
    else if (code < 0x00000800)
      buff += String.fromCharCode(((code>>06)&0x1F)+0xC0) +
          String.fromCharCode((code&0x3F)+0x80)
    else if (code < 0x00010000)
      buff += String.fromCharCode(((code>>12)&0x0F)+0xE0) +
          String.fromCharCode(((code>>06)&0x3F)+0x80) +
          String.fromCharCode((code&0x3F)+0x80)
    else if (code < 0x00200000)
      buff += String.fromCharCode(((code>>18)&0x07)+0xF0) +
          String.fromCharCode(((code>>12)&0x3F)+0x80) +
          String.fromCharCode(((code>>06)&0x3F)+0x80) +
          String.fromCharCode((code&0x3F)+0x80)
    else if (code < 0x04000000)
      buff += String.fromCharCode(((code>>24)&0x03)+0xF8) +
          String.fromCharCode(((code>>18)&0x3F)+0x80) +
          String.fromCharCode(((code>>12)&0x3F)+0x80) +
          String.fromCharCode(((code>>06)&0x3F)+0x80) +
          String.fromCharCode((code&0x3F)+0x80)
    else
      buff += String.fromCharCode(((code>>30)&0x01)+0xFC) +
          String.fromCharCode(((code>>24)&0x3F)+0x80) +
          String.fromCharCode(((code>>18)&0x3F)+0x80) +
          String.fromCharCode(((code>>12)&0x3F)+0x80) +
          String.fromCharCode(((code>>06)&0x3F)+0x80) +
          String.fromCharCode((code&0x3F)+0x80)
  }
  return buff;
}

Konverze z UTF-8

Postup je analogický. Nejdříve zjistíme, do kolika bajtů byl znak zakódován, a pak podle dané šablony provedeme bitovou transformaci. Následně nesmíme zapomenout přeskočit o příslušný počet bajtů vpřed.

function convertFROMutf8(text) {
  var code, zbyva, buff = „“;
  for(var i = 0; i<text.length; i++) {
    kod = text.charCodeAt(i)
    zbyva = text.length – i;
    if (kod < 128) {
      // standardní ASCII
    } else if ((kod & 0xE0) == 0xC0 && zbyva >= 2) {
      kod = ((kod & 0x1F) << 6) +
        ((text.charCodeAt(i + 1) & 0x3F));
      i += 1
    } else if ((kod & 0xF0) == 0xE0 && zbyva >= 3) {
      kod = ((kod & 0xF) << 12) +
        ((text.charCodeAt(i + 1) & 0x3F) << 06) +
        ((text.charCodeAt(i + 2) & 0x3F));
      i += 2
    } else if ((kod & 0xF8) == 0xF0 && zbyva >= 4) {
      kod = ((kod & 0x7) << 18) +
        ((text.charCodeAt(i + 1) & 0x3F) << 12) +
        ((text.charCodeAt(i + 2) & 0x3F) << 06) +
        ((text.charCodeAt(i + 3) & 0x3F));
      i += 3
    } else if ((kod & 0xFC) == 0xF8 && zbyva >= 5) {
      kod = ((kod & 0x3) << 24) +
        ((text.charCodeAt(i + 1) & 0x3F) << 18) +
        ((text.charCodeAt(i + 2) & 0x3F) << 12) +
        ((text.charCodeAt(i + 3) & 0x3F) << 06) +
        ((text.charCodeAt(i + 4) & 0x3F));
      i += 4
    } else if ((kod & 0xFC) == 0xF8 && zbyva >= 6) {
      kod = ((kod & 0x1) << 30) +
        ((text.charCodeAt(i + 1) & 0x3F) << 24) +
        ((text.charCodeAt(i + 2) & 0x3F) << 18) +
        ((text.charCodeAt(i + 3) & 0x3F) << 12) +
        ((text.charCodeAt(i + 4) & 0x3F) << 06) +
        ((text.charCodeAt(i + 5) & 0x3F));
      i += 5
    }
    buff += String.fromCharCode(kod);
  }
  return buff;
}

Uvedené postupy si můžete důkladně prozkoumat a vyzkoušet na testovací HTML stránce. Zde se jedná o konverzi mezi UTF-8 a vnitřním formátem prohlížeče (předpokládejme UCS-2). Pro převod do osmibitových znakových sad by se musely provést další úpravy dle principů popsaných v předchozích článcích.

Jak poznáme text v UTF-8?

Pokud se díváte do prohlížeče a vidíte, že český text není zobrazen správně, zkuste si všimnout, jak jsou původní české znaky nahrazeny. Pokud jsou nahrazeny dvěma či třemi jinými znaky, jde zcela určitě o text v UTF-8, o kterém si prohlížeč myslí, že jde o některé osmibitové kódování. Někdy se naopak můžou některé znaky ztrácet, pak jde o text v osmibitovém kódování, který prohlížeč považuje za UTF-8.

Shrnutí

Tento článek uzavírá třídílnou minisérii o znakových sadách a jejich použití v praxi. Doufám, že se vám získané vědomosti budou hodit, ať již při volbě nejvhodnější znakové sady pro vaše účely nebo při řešení složitějších úkolů, jako jsou právě konverze mezi různými druhy kódování. Nebyly zde sice probrány všechny, ovšem pro vývoj internetových aplikací by měl tento základ postačovat.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

3 Příspěvků v diskuzi

Odpovědět