Autor wpisu: Śpiechu, dodany: 08.01.2012 16:59, tagi: php
Jakiś taki naukowy ten tytuł wyszedł. Ale inaczej się chyba nie da. Potrzebowałem pretekstu żeby się trochę pobawić w manipulowanie bitami. Padło na Unicode z racji fajnego sposobu, w jaki wymyślono sam zapis znaków. Jeśli jest jeszcze ktoś kto nie wie co to jest Unicode to zapraszam do źródła.
Wszystko zostało tak przemyślane, że im bardziej pokręcony język, tym więcej miejsca potrzeba na jego zapisanie. Wszystkie liczby i litery bez ogonków damy radę zapisać w postaci 1 bajta. Chodziło o kompatybilność z formatem ASCII. Znaczki języka polskiego znajdują się w dziale Latin Extended-A. Zapis znaków polskich zajmie 2 bajty w formacie UTF-8.
Mając numer znaku z tabeli Unicode najłatwiej wyświetlić go za pomocą wbudowanej w PHP funkcji html_entity_decode()
:
echo html_entity_decode('&#' . 0xA7 . ';', ENT_NOQUOTES, 'UTF-8');
Wywołanie powyższego wiersza spowoduje wyświetlenie znaku o kodzie 0xA7 — paragrafu §. W jaki więc sposób wykonuje się czary-mary i znak Unicode staje się znakiem UTF-8? Weźmy na warsztat znak ę opisany w tablicy jako latin small letter e with ogonek. Ma numer 0119 (heksadecymalnie!), czyli można zapisać tak:
0x11916 = 28110 = 1000110012
Z powyższego widać, że liczbę dziesiętną 281 możemy zapisać w postaci 9 bitów. Dokumentacja funkcji utf8_encode()
zawiera tabelkę ile bitów znaku zmieścimy w ilu bajtach UTF-8. Wygląda na to, że w jednobajtowym zapisie zmieścimy znaki o numerach od 0 do 12710 (7 bitów). Za to dysponując 2 bajtami zapiszemy liczby aż do 204710, czyli w zupełności nam wystarczy.
Znak ę w UTF-8 zapisujemy w 2 bajtach, czyli do liczby 110000002 musimy dodać przesuniętą o 6 bitów w prawo liczbę 281 (ponieważ pójdą do drugiego bajtu znaku). Z drugiego wyrzucamy wszystko poza 6 ostatnimi bitami, a następnie dodajemy do liczby 100000002.
$uniChar = 0x119; $byte1 = $uniChar >> 6 | 0xC0; $byte2 = $uniChar & 0x3F | 0x80
Wyszło na to, że ę w zapisie UTF-8 to będzie 11000100 10011001. Po ubraniu tego wszystkiego w funkcję:
function hexToUTF8HexArray($hexNum) { // konwertujemy na liczbe w razie czego if (is_string($hexNum)) $hexNum = hexdec($hexNum); $hexArray = array(); if ($hexNum < 0x80) { $hexArray[0] = '0x' . dechex($hexNum); return $hexArray; } elseif ($hexNum < 0x800) { $hexArray[0] = '0x' . dechex($hexNum >> 6 | 0xC0); $hexArray[1] = '0x' . dechex($hexNum & 0x3F | 0x80); return $hexArray; } else { throw new Exception('Not supported'); } }
Wywołując hexToUTF8HexArray(0x119)
da nam tablicę z wartościami 0xc4 i 0x99. No dobra, a co jak chcę odwrócić proces? Teraz będzie przyjemniej:
function utf8ToUnicode(array $utf8Array) { // konwertujemy na liczby $utf8ArrayChecked = array(); foreach ($utf8Array as $utf8) { if (is_string($utf8)) $utf8 = hexdec($utf8); $utf8ArrayChecked[] = $utf8; } $bytesCount = count($utf8ArrayChecked); switch ($bytesCount) { case 1: return $utf8ArrayChecked[0]; case 2: // wyrzucamy naglowki $b1 = $utf8ArrayChecked[0] & 0x1F; $b2 = $utf8ArrayChecked[1] & 0x3F; // tutaj cala magia $number = $b1 << 6 | $b2; return '0x' . dechex($number); default: throw new Exception('Not supported'); } }
Wywołując utf8ToUnicode(array(0xc4, 0x99))
otrzymamy 0x119, czyli gra jak trzeba.
Na koniec ciekawostka: mając jakiś znak możemy łatwo wydobyć jego wartość UTF-8.
function charToUTF8HexArray($char) { $i = 0; $hexArray = array(); while (isset($char[$i])) { $hexArray[] = '0x' . dechex(ord($char[$i++])); } return $hexArray; }
charToUTF8HexArray('ę')
da nam tablicę 0xc4, 0x99.