Rémtörténet a karakterkódolásokról

Czirkos Zoltán, Dobra Gábor · 2022.05.25.

Ékezetes betűk, szövegek kódolása és megjelenítése a programokban. Javasolt olvasmány azoknak, akik szeretnék a nagy házijukban megoldani a magyar ékezetes szövegek helyes kezelését.

18+

Az ékezetes betűk kódolásával máig gondok vannak. Sokféle szabvány létezik arra, hogy mely ékezetes betűt milyen számkóddal jelölünk, ami azért nehéz ügy, mert ezek a kódtáblázatok általában egymással inkompatibilisek.

A probléma ugyan elméletben megoldott, létezik olyan karakterkódolás a Unicode szabvány részeként, amely a világ (majdnem) összes nyelvének (majdnem) összes írásjeléhez karakterkódot rendel, mégis rendszeresen találkozunk árvíztûrõ tükörfúrógépekkel (meg ĂĄrvĂ­ztĹąrĹ tükörfúrógépekkel) még nyomtatott szövegekben is. Ennek oka sokszor a programozók figyelmetlensége. Sajnos a Windows is hírhedten hibás és hiányos ilyen téren.

A karakterkódolási szabványok követésével és a programok helyes beállításával a problémák megszüntethetők. Legtöbbször csak egy-két függvényhívásról van szó. Ha a rémtörténet nem érdekel, szeretnél jól aludni, vagy csak azért vagy itt, mert az ékezetes szövegek nem látszanak jól a nagy házidban, akkor lapozz az oldal legaljára, a receptekhez.

1. Az egybájtos karakterkódolások

Az angol nyelvben használt, ékezet nélküli betűkhöz az ASCII kódolás terjedt el. Erről előadáson is volt szó. Egykor voltak más kódolások is, de a ASCII mára gyakorlatilag egyeduralkodóvá vált.

A nyugat-európai nyelvekhez (pl. a franciához) használják ennek a Latin-1, vagy más néven ISO8859-1-es bővítését. Ez az ASCII kódolás 128 kódját újabb 96 karakterrel egészíti ki a 160-255 tartományban, így ez már 8 bites. Ebben sajnos nincsen benne a magyar ő és ű betű. A testvérében, a Latin-2-ben (ISO8859-2) benne van, így ezzel bármilyen magyar szöveg leírható. Ebben a magyar ű betű helyén a Latin-1-esben û van, az ő helyén pedig õ. Ezért találkozni néha ilyenekkel: árvíztûrõ tükörfúrógép, amikor egy Latin-2 kódolással megadott sztringet Latin-1 kódolásúnak gondol egy program, vagy esetleg egy betűtípusban szerepel helytelenül, hogy melyik „alakzat” (graféma) melyik karaktert is jelenti.

A Latin-2-höz hasonló kódolást használ a Windows a szövegfájloknál (Windows-1250). A konzol ablakban sajnos egy másikat (IBM-852), amely a Latin-1-2-re egyáltalán nem hasonlít. Ezek a kódolások a lenti képeken láthatóak.

ISO8859-1 (Latin-1)
ISO8859-2 (Latin-2)
IBM-852

A karakterkódolások közötti inkompatibilitás problémája akkor jelentkezik, amikor a programunkban ékezetes szöveget szeretnénk kiírni. Ha azt mondjuk a Code::Blocksban (Windowson), hogy printf("ő"), a keletkező sztring a 0xF5, 0x00 bájtokból áll: az ő kódja és a lezáró nulla. De a konzolablakban a 0xF5 a paragrafus jel § karakterkódja! Ha kérünk egy sztringet, az viszont helyesen fog megjelenni kiíráskor, mivel a programunkban történő beolvasáskor már az IBM-852 szerinti kódok vannak:

═rd be, hogy teniszŘt§!
teniszütő
Ezt Ýrtad be: teniszütő

Ha a konzol ablakhoz kiválasztunk egy olyan betűtípust, amely tartalmazza a megfelelő ékezetes karaktereket (pl. a Consolas és a Lucida Console ilyen), és a parancssorban a program futtatása előtt átváltjuk a karakterkódolást arra, amelyik kódolással a forráskódot is elmentettük, helyesen jelenhet meg a szöveg. Ehhez a lenti recepteknél a konzolos programokhoz írt útmutatót kell használni.

2. A Unicode által definiált kódolás

A többnyelvű szövegek nem írhatóak le a fenti kódolásokkal. Nem csak az a baj, hogy egy cirill vagy japán betűk nem szerepelnek bennük, hanem például még egy latin betűs útikönyvvel is gondban vagyunk! A Latin-1-ben nincs ő, a Latin-2-ben nincs ø, ezért ez a mondat nem írható le egyikkel sem: Dánia fővárosa København.

A '80-as évek vége táján felmerült, hogy létre kellene hozni egy olyan kódtáblát, amely a világ összes nyelvének összes karakterét tartalmazza, mert akkor nem lesz ilyen gond. Ez lett a Unicode szabvány része. Mivel azonban az összes létező írásjelek 256-nál többen vannak, ebben egy karaktert már nem egy bájttal, hanem egy nagyobb számmal jelölnek. Aminek pedig az a következménye, hogy egy Unicode sztring közvetlenül nem jelenhet meg char tömbként a C programunkban, mert a char a C fogalmai szerint bájtot jelent.

A Unicode szabványban a legtöbb karakter elfér 16 biten, de újabb verziókban akár még nagyobbak lehetnek. Míg pl. az ő betű vagy az € jel ábrázolható 16 bites számmal (karakterkódjuk 337 és 8364), addig más jelekhez, pl. emoji-khoz 216, azaz 65536 feleti szám tartozik (😺 kódja 128570, 🤡 pedig 129313).

Az egybájtos karakterkódokról Unicode-ra átalakítani egy szöveget nagyon könnyű; egy 256 elemű tömbben eltárolhatjuk, melyik kódból mi lesz. Az egyes kódolásokhoz (Latin-1, Latin-2 stb.) azonban eltérő táblázatok tartoznak. A visszaalakítás nem ilyen egyszerű, mert bár technikailag könnyen megvalósítható, könnyen előfordulhat, hogy olyan karaktert kell átkódolni, ami a cél kódtáblában nem létezik. A halmazelméletben használt ∉ „nem eleme” szimbólum például semelyik fenti táblázatban nem létezik.

Fölmerül még egy probléma az egy bájtnál nagyobb számok miatt. Egyes számítógéptípusok úgy tárolják a 16 bites számokat – amelyeket két 8 bites bájtként kell elhelyezni a memóriában –, hogy az alsó 8 bitet írják előbb, és utána a felső 8-at. Vagyis előbb a kicsit (little endian). Más gépek meg épp fordítva, előre veszi a felső 8 bitet, és utána, a következő memóriarekeszbe pedig az alsó 8 bitet (big endian). Ez egészen addig nem gond, amíg két, eltérő típusú számítógépnek nem kell kommunikálnia egymással. Viszont ha ezek az Interneten keresztül adatot küldenének egymásnak, vagy szeretnék olvasni az egymás által kiírt fájlokat, akkor már figyelni kell arra, hogy esetleg nem ugyanazt a bájtsorrendet használják – különben amit az egyik 0xFCE2-nek mond, azt a másik 0xE2FC-nek fogja értelmezni, és fordítva. Nagyobb számok (pl. 32 bites integerek) esetén hasonló a helyzet.

Ezért a Unicode kódolású szövegekben el szoktak helyezni egy ún. BOM (byte order mark, bájtsorrend jele) karaktert, amelynek a kódja 0xFEFF. Ha a szöveget olvasó számítógép egy 0xFEFF kódot talál a szövegben, akkor tudja, hogy annak bájtsorrendje megegyezik a sajátjával. Ha azonban egy 0xFFFE számot lát (amely szándékosan semmilyen karakternek nem kódja), akkor tudja, hogy minden számban meg kell cserélnie a felső és alsó nyolc bitet.

A BOM-mal kiegészített, „HELLO” szöveget tároló fájlok bájtsorrendtől függően így nézhetnek ki (16 bites tömbelemeket feltételezve):

FE FF 00 48 00 45 00 4C 00 4C 00 4F
FF FE 48 00 45 00 4C 00 4C 00 4F 00

3. Az UTF-8 kódolás

A Unicode kódolás elméletben visszafelé kompatibilis az ASCII kódolással, ugyanis az első 128 karaktere ugyanabban a sorrendben van. Azonban a szövegfájlok a bájtsorrend miatt mégsem kompatibilisek egymással. Ezért találták ki az UTF-8 szövegkódolást. Az ilyen szövegekben a Unicode kódszámokat használják, azonban mindig 8 bites értékekből építik fel azt, átalakítva a nagyobb számokat több bájtos sorozatokká. Ha a leírandó kódszám elfér 7 biten (vagyis 0x00 és 0x7F között van), akkor le kell vágni 8 bitre, és úgy tenni a fájlba. Ha ennél nagyobb, akkor kettő, három, sőt néha még több bájtos sorozattal írható le. A bájtok sorrendje azonban az ilyen sorozatokban kötött, tehát nem függ a számítógép típusától. Az átkódolás az alábbi módon helyezi el a biteket:

TartományUnicodeUTF-8
   00-  7F00000000 0xxxxxxx0xxxxxxx
   80-07FF00000yyy yyxxxxxx110yyyyy 1xxxxxxx
 0800-FFFFzzzzyyyy yyxxxxxx1110zzzz 10yyyyyy 1xxxxxxx
10000–......

A Wikipedia az Euró jelét hozza példának, hogyan néz ki egy karakter UTF-8 kódolása:

  • Az € karakter kódszáma 0x20AC.
  • Ez binárisan 0010000010101100, ami a fenti táblázat alapján a harmadik kategóriába esik. Vagyis három bájton lesz kódolható.
  • Az első bájt viszi az első négy bitet: 11100010. A második a következő hatot: 10000010. Az utolsó a maradékot: 10101100.
  • A kapott bájtok: 0xE2 0x82 0xAC.

Az UTF-8 kódolású sztringek, mivel bájtokból állnak, a C forráskódokban „újra” char[] tömbként jelenhetnek meg. Ezeknél azonban a beépített sztringkezelő függvényeket használva elég furcsa dolgokat tapasztalhatunk. Pl. az strlen() függvény szerint "o" hossza 1, viszont "ő" hossza 2, végül "€" esetében 3 a számított hossz. Az ő betűt két bájt kódolja, míg az o betűt csak egy, és ezt az strlen() nem tudja. Megkell szokni, a char sajnos ilyenkor nem karaktert, hanem bájtot jelent. Még jó, hogy a többi függvény, pl. a strcpy() és a strcmp() nagyjából helyesen működik. Végülis ez volt a célja az UTF-8 megalkotóinak.

Egy Unicode kódolású szöveget UTF-8 bájtsorozattá alakítani könnyű, néhány bitműveletről van szó:

#include <stdio.h>
#include <stdint.h>
#ifdef _WIN32
    #include <windows.h>
#endif

/* Unicode sztringbol UTF-8 sztringet csinal.
 * A bemenet es a kimenet is nullaval terminalt tomb.
 * A kodolas csak 16 bites karakterekre mukodik. */
void unicode_2_utf8(uint16_t const *be, uint8_t *ki) {
    int pk = 0;
    for (int pb = 0; be[pb] != 0; ++pb) {
        if (be[pb] <= 0x007F) {
            // 00000000 0xxxxxxx, 0x0000-0x007F
            // 0xxxxxxx
            ki[pk++] = be[pb];
        } else if (be[pb] <= 0x07FF) {
            // 00000yyy yyxxxxxx, 0x0080-0x07FF
            // 110yyyyy 10xxxxxx
            ki[pk++] = 0xC0 | be[pb] >> 6;      // 0xC0 = 11000000
            ki[pk++] = 0x80 | (be[pb] & 0x3F);  // 0x80 = 10000000, 0x3F = 00111111
        } else {
            // zzzzyyyy yyxxxxxx, 0x0800-0xFFFF
            // 1110zzzz 10yyyyyy 10xxxxxx
            ki[pk++] = 0xE0 | be[pb] >> 12;     // 0xE0 = 11100000
            ki[pk++] = 0x80 | ((be[pb] >> 6) & 0x3F);
            ki[pk++] = 0x80 | (be[pb] & 0x3F);
        }
    }

    ki[pk] = 0;
}

int main(void) {
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);
#endif

    uint16_t arvizturo[] = { 0x00E1, 'r', 'v', 0x00ED, 'z', 't',
                             0x0171, 'r', 0x0151, ' ', 0x263A, ' ', 0x20AC,
                             0x0000 };
    uint8_t arvizturo_utf8[30];

    unicode_2_utf8(arvizturo, arvizturo_utf8);
    printf("arvizturo szmajli, es euro: %s\n", arvizturo_utf8);
}

A visszaalakítás ugyanilyen egyszerű. A programot elindítva ennek kell megjelennie: árvíztűrő ☺ €. Linuxon egyből ez fog megjelenni (ez UTF-8 kódolást használ szinte mindenhol), a Windowsokon meg a kódlapot át kell állítani, a lentebbi recepteknek megfelelően.

4. Ékezetek: receptek

Az alábbi kódrészletek a nagy házikban szabadon felhasználhatóak.

Linux konzol ablak és fájlok

Nincs különösebb teendő, minden működik magától. A legtöbb Linux UTF-8 kódolást használ a parancssori ablakokban és a fájlokban is, úgyhogy semmi extra teendő nincsen, rögtön működnek az ékezetes betűt használó programok. Egy dologra kell figyelni, hogy az UTF-8-ban karakter≠bájt! Mivel az ékezetes betűk kettő, egyéb karakterek akár több bájton lehetnek kódolva, a sztringek indexei elcsúsznak, és hosszaik nem egyeznek meg az strlen() által adottakkal. Például strlen("teniszütő") értéke 11. Ez 9 karakter, 11 bájt hosszú sztring, 12 bájtnyi memóriafoglalás. (Az utf8_strlen() függvény megírása házi feladat.)

Windows konzol ablak – Windows-1250 kódolással

Bár a Windows már az XP verzió óta támogatja az 1993 óta létező UTF-8 szabványt, alapbeállítás esetén még mindig nem használja. A Windows 10-ben az UTF-8 támogatása konkrétan hibás, így kénytelenek vagyunk a múltat konzerválni, és Windows-1250 karakterkódolást használni.

A teendők ehhez:

  • A parancssori ablakot úgy beállítani, hogy Consolas, vagy egy másik, Unicode-kompatibilis betűtípust használjon. Az ablak ikonjára klikk, Alapértelmezések (Defaults), Betűtípus (Font). A következő ablak megnyitásakor jó lesz.
  • Figyelni kell arra, hogy a forráskódok Windows-1250 kódolással legyenek elmentve. Ehhez a Code::Blocks Edit / File encoding menüpontja alatt a System default lehetőséget kell kiválasztani. Ha van olyan korábban létrehozott fájlunk, amiben már vannak ékezetes karakterek, csak nem jól jelennek meg, át kellhet konvertálni a fájlt, annak megnyitásával és újbóli elmentésével.
  • Ha a program használ szöveges adatfájlokat, azokat a fentiekhez hasonlóan Windows-1250 kódolással kell elmenteni.
  • A program elején kiválasztani ezt a kódolást a lenti kódrészlettel.

A konzol ablak kódlapjának beállítása megtehető két függvényhívással: SetConsoleCP(1250) és SetConsoleOutputCP(1250). Az egyik a bemeneti kódlapot állítja be, a másik pedig a kimeneti kódlapot. (Hogy miért tér el a beolvasáskor és kiíráskor használt karakterkódolás a Windowsban, miért kell ezeket külön beállítani, egy örök rejtély a világ számára.) Vigyázat, ezek nem szabványos függvényhívások! Illik őket #ifdef-ek közé tenni, hogy maradjanak hatástalanok, ha más operációs rendszeren fordítja valaki a programot. A két függvényhívást elég a program elején egyszer megtenni (praktikusan a main() elején valamikor), többször már nem.

#include <stdio.h>
#ifdef _WIN32
    #include <windows.h>
#endif


int main(void) {
#ifdef _WIN32
    SetConsoleCP(1250);
    SetConsoleOutputCP(1250);
#endif

    printf("Írd be, hogy teniszütő!\n");
    char s[100];
    scanf("%s", s);
    printf("Ezt írtad be: %s.", s);

    return 0;
}

A Windows-1250 kódolással mentés könnyen ellenőrizhető. Csak meg kell vizsgálni, hogy egy adott betű karakterkódja egyezik-e az általunk elvárttal:

#include <assert.h>

int main(void) {
    assert((unsigned char)'ő' == 245);
}

Ügyelni kell arra, hogy bizonyos karakterek (pl. ☺) nem jeleníthetők meg ezzel a kódolással. Ilyenkor a Code::Blocks automatikusan átáll UTF-8-ra, különben el sem tudná menteni a fájlt! A Code::Blocks-nak néha az ő betűvel is gondja akad, pl. angol nyelvű Windowson. Ha mentés közben a lentebb látható figyelmeztető ablak ugrik fel, akkor a Windows nincs magyar nyelvűre állítva, vagy esetleg a program valamelyik sztringje olyan karaktert tartalmaz, ami nem elmenthető ilyen formában. A magyar nyelvűre állításhoz a vezérlőpult területi beállításai (Region) között kell kutakodni.

Figyelmeztető üzenet a hibás karakterkódolásról
Vezérlőpult, területi beállítások

SDL – UTF-8 kódolással

Az SDL-es programoknál érdemes inkább UTF-8 karakterkódolást használni. Az SDL_TTF könyvtár TTF_RenderUTF8_Blended és hasonló függvényei közvetlenül is támogatják ezt a kódolást.

Arra kell csak figyelni, hogy a program forráskódja és a szöveges adatfájljai is UTF-8 kódolással legyenek elmentve. Ehhez a Code::Blocks Edit / File encoding menüpontja alatt az UTF-8 lehetőséget kell kiválasztani. Ha van olyan korábban létrehozott fájlunk, amiben már vannak ékezetes karakterek, csak nem jól jelennek meg, akkor át kellhet konvertálni a fájlt, annak megnyitásával és újbóli elmentésével.

Az UTF-8 kódolással mentés könnyen ellenőrizhető:

#include <assert.h>
#include <string.h>

int main(void) {
    assert(strlen("ő") == 2);
}

UTF-8 BOM karakter a Windows szövegfájljaiban

Az UTF-8 kódolás gyakorlatilag a Unicode karakterek kódjait használja, azoknak egy kényelmesebb ábrázolási módja. Azt azonban nem köti meg az UTF-8 szabvány, hogy a Unicode fájlok elején lévő BOM-ot tartalmaznia kell-e egy UTF-8 fájlnak. Mivel a bájtok sorrendje kötött, teljesen felesleges jelezni a bájtsorrendet, így a legtöbb program nem használ UTF-8 kódolás esetén BOM-ot.

Néhány windowsos program (pl. Notepad) ennek ellenére elhelyezi ezt a bájtot a fájlok elejére, ezzel számos problémát okozva. Sok program erre nincs felkészítve, hiszen logikátlan a dolog. Ironikus módon az Internet Exporer is ilyen, pedig az is a Windows része.

A BOM kódja 0xFEFF, ami a 0x0800-0xFFFF tartományba esik, így UTF-8 reprezentációja három bájtos: EF BB BF. Pl. az „árvíztűrő” szöveg egy szövegfájlban:

C3 A1 72 76 C3 AD 7A 74 C5 B1 72 C5 91          (UTF-8)
EF BB BF C3 A1 72 76 C3 AD 7A 74 C5 B1 72 C5 91 (UTF-8 + BOM)

Ha ilyet látunk a fájl elején, egyszerűen el kell dobni az első három bájtot.

char buf[3];
fscanf(fp, "%3c", buf);
if (memcmp(buf, "\xEF\xBB\xBF", 3) != 0) /* ha nem bom-mal kezdődik a fájl */
    fseek(fp, 0, SEEK_SET);              /* vissza az elejére */
/* ... a fájl kezelése ... */

5. Konverziós függvények

Konverzió: Latin-2-ből Unicodeba

Ha egy Windowson egy Latin-2 kódolású fájlból beolvasott szöveget kell megjeleníteni az SDL-lel, akkor ilyen irányú átalakítást kell csinálni. Az alábbi függvénnyel oldható meg:

/* Latin2 -> 16 bites Unicode atalakitas. Az SDL is ilyet
 * hasznal, az uint16_t az SDL-beli Uint16-tal egyenerteku. */
void latin2_2_unicode(char *be, uint16_t *ki) {
    static uint16_t tabla[128] = {
        /* A 0x80-0xFF karakterek Unicode megfeleloje */
        0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
        0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, 0x93,
        0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D,
        0x9E, 0x9F, 0xA0, 0x104, 0x2D8, 0x141, 0xA4, 0x13D, 0x15A,
        0xA7, 0xA8, 0x160, 0x15E, 0x164, 0x179, 0xAD, 0x17D, 0x17B,
        0xB0, 0x105, 0x2DB, 0x142, 0xB4, 0x13E, 0x15B, 0x2C7, 0xB8,
        0x161, 0x15F, 0x165, 0x17A, 0x2DD, 0x17E, 0x17C, 0x154,
        0xC1, 0xC2, 0x102, 0xC4, 0x139, 0x106, 0xC7, 0x10C, 0xC9,
        0x118, 0xCB, 0x11A, 0xCD, 0xCE, 0x10E, 0x110, 0x143, 0x147,
        0xD3, 0xD4, 0x150, 0xD6, 0xD7, 0x158, 0x16E, 0xDA, 0x170,
        0xDC, 0xDD, 0x162, 0xDF, 0x155, 0xE1, 0xE2, 0x103, 0xE4,
        0x13A, 0x107, 0xE7, 0x10D, 0xE9, 0x119, 0xEB, 0x11B, 0xED,
        0xEE, 0x10F, 0x111, 0x144, 0x148, 0xF3, 0xF4, 0x151, 0xF6,
        0xF7, 0x159, 0x16F, 0xFA, 0x171, 0xFC, 0xFD, 0x163, 0x2D9
    };

    int j = 0;
    for (int i = 0; be[i] != 0; ++i) {
        if (be[i] < 128) /* ascii? */
            ki[j++] = be[i];
        else
            ki[j++] = tabla[be[i] - 128];
    }
    ki[j] = 0;
}

Konverzió: Unicode-ból UTF-8-ba

Ha egy SDL-en, billentyűzetről beolvasott sztringet kell fájlba írni, vagy más, meglévő UTF-8 sztringekbe beilleszteni:

/** Unicode -> UTF-8, legfeljebb 16 bitre. */
void unicode_2_utf8(uint16_t const *be, uint8_t *ki) {
    int pk = 0;
    for (int pb = 0; be[pb] != 0; ++pb) {
        if (be[pb] <= 0x007F) {
            // 00000000 0xxxxxxx, 0x0000-0x007F
            // 0xxxxxxx
            ki[pk++] = be[pb];
        } else if (be[pb] <= 0x07FF) {
            // 00000yyy yyxxxxxx, 0x0080-0x07FF
            // 110yyyyy 10xxxxxx
            ki[pk++] = 0xC0 | be[pb] >> 6;      // 0xC0 = 11000000
            ki[pk++] = 0x80 | (be[pb] & 0x3F);  // 0x80 = 10000000, 0x3F = 00111111
        } else {
            // zzzzyyyy yyxxxxxx, 0x0800-0xFFFF
            // 1110zzzz 10yyyyyy 10xxxxxx
            ki[pk++] = 0xE0 | be[pb] >> 12;     // 0xE0 = 11100000
            ki[pk++] = 0x80 | ((be[pb] >> 6) & 0x3F);
            ki[pk++] = 0x80 | (be[pb] & 0x3F);
        }
    }
    ki[pk] = 0;
}

Konverzió: UTF-8-ból Unicode-ba

Ha UTF-8 sztringek vagy fájlok karaktereit kellene egyesével látni:

/* UTF-8 bajtsorozatbol allitja elo az Unicode sztringet.
 * Mindketto nullaval terminalt.
 * Nincs hibakezeles: a bemeneti bajtsorozatnak helyesnek kell lennie,
 * es legfeljebb 3 bajtos szekvenciakat tartalmazhat! */
void utf8_2_unicode(uint8_t const *be, uint16_t *ki) {
    int pk = 0;
    for (int pb = 0; be[pb] != 0; ++pb) {
        if (be[pb] < 0x80) {
            // 00000000 0xxxxxxx, 0x0000-0x007F
            // 0xxxxxxx
            ki[pk++] = be[pb];
        }
        else if (be[pb] >> 5 == 6) { /* 0x6 = 110 bin */
            // 00000yyy yyxxxxxx, 0x0080-0x07FF
            // 110yyyyy 10xxxxxx
            ki[pk++] = (be[pb] & 0x1f) << 6 | (be[pb + 1] & 0x3f);
            pb += 1;            /* ket bajtot hasznaltunk */
        }
        else {
            // zzzzyyyy yyxxxxxx, 0x0800-0xFFFF
            // 1110zzzz 10yyyyyy 10xxxxxx
            ki[pk++] = (be[pb] & 0x0f) << 12
                       | (be[pb + 1] & 0x3f) << 6
                       | (be[pb + 2] & 0x3f);
            pb += 2;            /* harom bajtot hasznaltunk */
        }
    }
    ki[pk] = 0;
}