giovedì 18 aprile 2013

PHP e stringhe con caratteri strani ossia il charset UTF-8 e dintorni nel PHP (Parte 1/4)

Il carattere sostitutivo UTF-8
per le codifiche errate
Quante volte capita di vedere nei propri progettini PHP il carattere  nell'immagine qui accanto? Oppure dei caratteri accentati che nulla hanno a che vedere con le stringhe che ci si aspetta di visualizzare?

Di norma è solo un problema di charset. Già ma cos'è un charset?

Ogni carattere presente sullo schermo ha necessità di essere rappresentato nel computer sotto forma di una sequenza di bit. Se si pensasse ad un carattere, o glifo inteso come rappresentazione grafica di un elemento, come un disegno inserito in una matrice di punti 8x8, avremmo che ogni carattere è composto da 64 punti per ognuno dei quali dovremmo indicare se acceso o spento (0 o 1 ossia 1 bit). Ogni carattere sarebbe rappresentato da 8 byte. Un'enormità per un solo carattere! La cosa è ben peggiore se si considera che i font attualmente utilizzati dai PC di tipo true type, sono disegnati tramite una breve sequenza d'istruzioni che descrivono il carattere stesso permettendone il disegno a diverse dimensioni senza perdere in definizione.

Qual'è il modo quindi? Associare ad ogni glifo un indice. Dato tale indice è possibile tramite un font ottenerne il glifo corrispondente.

In principio fu la codifica ASCII. ASCII è un sistema di codifica a 7 bit che permette di mappare (associazione chiave-valore) fino a 128 valori (glifi nel caso specifico). In particolare i valori da 0 a 31 sono caratteri di controllo cui non corrisponde alcun glifo. Da 32 a 127 è fatto corrispondere ad ogni valore un preciso glifo disegnato sullo schermo a rappresentare un carattere. Il 128 è il CANC o DEL cui non corrisponde alcun glifo.

Alla codifica a 7 bit seguì ben presto quella a 8 bit, con cui si avevano altri 128 caratteri da poter codificare. Regnava però grande confusione, dato che ogni linguaggio naturale ha bisogno di una propria specifica estensione. In più ci si mettevano anche i produttori inventando nuove mappature.

Da allora si è cercato di fare un po' di chiarezza ed ordine (si fa per dire) con UCS (Universal Character Set), più precisamente ISO/IEC 10646, e Unicode. Per entrambi la mappature dei primi 127 caratteri è esattamente la stessa, e corrisponde alla codifica ASCII. Per la restante parte la mappatura dei due standard è simile adottando a parità di indice lo stesso glifo.

ISO e Unicode hanno anche cercato di collaborare per unificare i charset tra cui l'ISO8859. Un'altro charset largamente diffuso, composto in realtà da diverse mappature ognuna delle quali può essere considerata un charset. Ad esempio ISO8859-1 anche noto come Latin1, codifica a 8 bit per l'europa occidentale, oppure ISO8859-2 codifica a 8 bit per l'europa centrale, ecc. fino al ISO8859-16. In definitiva un vero macello.

Unicode prevede l'utilizzo di 21 bit con cui associare ad un indice numerico un glifo. Attualmente solo gli indici da 0 a 10FFFF esadecimale sono utilizzati. Non tutti i valori hanno un glifo assegnato, e ad alcuni indici corrispondono dei caratteri di controllo che non hanno un corrispondente visibile. In più dipendono dalla versione Unicode che si prende in considerazione e dall'elaboratore in uso.

Ed UTF-8 in tutto questo dov'è? UTF-8 è, più che un charset, un metodo per memorizzare l'indice Unicode, o UCS se vogliamo, cui è associato un glifo. Non a caso UTF sta per Unicode Transformation Format. In particolare UTF-8 può occupare a 1 a 4 byte per un glifo. Se l'indice Unicode da memorizzare con la codifica UTF-8 è compreso tra 0 e 7F esadecimale (0-127 decimale ossia utilizza al più 7 bit), allora UTF-8 utilizza un solo byte per la memorizzazione dell'indice Unicode. Essendo Unicode compatibile con ASCII, come diretta conseguenza abbiamo la compatibilità della codifica UTF-8 con la codifica ASCII. Quindi, ad esempio il carattere A, avente codifica Unicode 65 decimale e 1000001 binario, può essere memorizzato da un elaboratore elettronico come sequenza binaria 01000001 occupando un byte. Quindi un qualunque flusso di byte codificato ASCII può essere interpretato senza problemi come UTF-8.

Se la codifica UCS/Unicode eccede i 7 bit, allora si utilizzano 2 byte per rappresentare l'indice Unicode in UTF-8. Il byte alto deve iniziare con 110xxxxx seguito dai 5 bit alti dell'indice in notazione binaria, metre il byte basso, essendo elemento di una catena di più byte, deve iniziare con 10xxxxxx seguito dai 6 bit più bassi dell'indice Unicode. Ciò significa che si hanno a disposizione 11 bit per codificare indici Unicode ossia da 80 a 7FF esadecimale.

Se gli undici bit non fossero più sufficienti, allora la codifica UTF-8 utilizza 3 byte in cui il primo inizia con 1110xxxx, mentre i successivi iniziano con 10xxxxxx. Ciò significa che si hanno a disposizione 4 bit nel byte alto e 12 bit nei due byte più bassi (6 bit per ogni byte impiegato) per un totale di 16 bit con cui codificare gli ulteriori indici Unicode.

Nella forma a 4 byte avremo il primo byte contenente 11110xxx seguito da tre byte nella forma 10xxxxxx, per complessivi 21bit.

Riepilogando
Unicode

bit

richiesti
Codifica UTF-8
da
a
Byte 4
Byte 3
Byte 2
Byte 1
0
7F
7
-
-
-
0xxxxxxx
80
7FF
11
-
-
110xxxxx
10xxxxxx
800
FFFF
16
-
1110xxxx
10xxxxxx
10xxxxxx
10000
10FFFF
21
11110xxx
10xxxxxx
10xxxxxx
10xxxxxx

Il numero di uno seguiti da uno 0 nel primo byte non è casuale, ma indica il numero di byte che compongono il carattere. Invece gli elementi che iniziano con 10 si presentano come appartenenti ad una catena di byte che compongono un carattere UTF-8.  Fanno eccezione i caratteri rappresentati da un byte che iniziano con 0.

Unicode utilizza gli indici da D800 fino a DFFF per le coppie surrogate della codifica UTF-16. Pertanto tali valori non hanno senso in UTF-8. Inoltre nella codifica di un carattere in UTF-8 non si possono ottenere gli ottetti C0, C1, F5 o FF.