Conversione di caratteri denormalizzati con UTF8String
Durante la conversione di emoji codificati in UTF-8 in una stringa non abbiamo ottenuto i caratteri corretti utilizzando UTF8ToString. Riceviamo questi caratteri UTF8 da un'interfaccia esterna. Abbiamo testato i caratteri UTF con un decodificatore UTF8 online e abbiamo visto che contengono i caratteri corretti. Sospetto che siano personaggi compositi.
procedure TestUTF8Convertion;
const
utf8Denormalized: RawByteString = #$ED#$A0#$BD#$ED#$B8#$85#$20 + #$ED#$A0#$BD#$ED#$B8#$86#$20 + #$ED#$A0#$BD#$ED#$B8#$8A;
utf8Normalized: RawByteString = #$F0#$9F#$98#$85 + #$F0#$9F#$98#$86 + #$F0#$9F#$98#$8A;
begin
Memo1.Lines.Add(UTF8ToString(utf8Denormalized));
Memo1.Lines.Add(UTF8ToString(utf8Normalized));
end;
Uscita in Memo1:
Denormalizzato:
Normalizzato: 😅😆😊
La scrittura della propria funzione di conversione basata sulla funzione WinApi MultiByteToWideCharnon ha risolto questo problema.
function UTF8DenormalizedToString(s: PAnsiChar): string;
var
pwc: PWideChar;
len: cardinal;
begin
GetMem(pwc, (Length(s) + 1) * SizeOf(WideChar));
len := MultiByteToWideChar(CP_UTF8, MB_PRECOMPOSED, @s[0], -1, pwc, length(s));
SetString(result, pwc, len);
FreeMem(pwc);
end;
Risposte
Se hai dati CESU-8 in un buffer e devi convertirli in UTF-8, puoi sostituire le coppie surrogate con un singolo carattere codificato UTF-8. Il resto dei dati può essere lasciato invariato.
In questo caso, la tua emoji è questa:
- punto di codice: 01 F6 05
- UTF-8: F0 9F 98 85
- UTF-16: D8 3D DE 05
- CESU-8: ED A0 BD ED B8 85
Il surrogato alto in CESU-8 ha questi dati: $ 003D
E il surrogato basso in CESU-8 ha questi dati: $ 0205
Come hanno sottolineato Remy e AmigoJack, troverai questi valori quando decodifichi la versione UTF-16 dell'emoji.
Nel caso di UTF-16 dovrai anche moltiplicare il $003D value by $400 (shl 10), aggiungi il risultato a $0205 and then add $10000 al risultato finale per ottenere il punto di codice.
Una volta ottenuto il punto di codice, puoi convertirlo in un set di valori UTF-8 a 4 byte.
function ValidHighSurrogate(const aBuffer: array of AnsiChar; i: integer): boolean;
var
n: byte;
begin
Result := False;
if (ord(aBuffer[i]) <> $ED) then exit; n := ord(aBuffer[i + 1]) shr 4; if ((n and $A) <> $A) then exit; n := ord(aBuffer[i + 2]) shr 6; if ((n and $2) = $2) then Result := True; end; function ValidLowSurrogate(const aBuffer: array of AnsiChar; i: integer): boolean; var n: byte; begin Result := False; if (ord(aBuffer[i]) <> $ED) then
exit;
n := ord(aBuffer[i + 1]) shr 4;
if ((n and $B) <> $B) then
exit;
n := ord(aBuffer[i + 2]) shr 6;
if ((n and $2) = $2) then
Result := True;
end;
function GetRawSurrogateValue(const aBuffer: array of AnsiChar; i: integer): integer;
var
a, b: integer;
begin
a := ord(aBuffer[i + 1]) and $0F; b := ord(aBuffer[i + 2]) and $3F;
Result := (a shl 6) or b;
end;
function CESU8ToUTF8(const aBuffer: array of AnsiChar): boolean;
var
TempBuffer: array of AnsiChar;
i, j, TempLen: integer;
TempHigh, TempLow, TempCodePoint: integer;
begin
TempLen := length(aBuffer);
SetLength(TempBuffer, TempLen);
i := 0;
j := 0;
while (i < TempLen) do
if (i + 5 < TempLen) and ValidHighSurrogate(aBuffer, i) and
ValidLowSurrogate(aBuffer, i + 3) then
begin
TempHigh := GetRawSurrogateValue(aBuffer, i);
TempLow := GetRawSurrogateValue(aBuffer, i + 3);
TempCodePoint := (TempHigh shl 10) + TempLow + $10000; TempBuffer[j] := AnsiChar($F0 + ((TempCodePoint and $1C0000) shr 18)); TempBuffer[j + 1] := AnsiChar($80 + ((TempCodePoint and $3F000) shr 12)); TempBuffer[j + 2] := AnsiChar($80 + ((TempCodePoint and $FC0) shr 6)); TempBuffer[j + 3] := AnsiChar($80 + (TempCodePoint and $3F));
inc(j, 4);
inc(i, 6);
end
else
begin
TempBuffer[j] := aBuffer[i];
inc(i);
inc(j);
end;
Result := < save the buffer here >;
end;
- UTF-8 è costituito da 1, 2, 3 o 4 byte per carattere. Il codepoint U + 1F605 è codificato correttamente come .
#$F0#$9F#$98#$85 - UTF-16 consiste di 2 o 4 byte per carattere. Le sequenze di 4 byte sono necessarie per codificare codepoint oltre U + FFFF (come la maggior parte degli Emoji). Solo UCS-2 è limitato ai codepoint da U + 0000 a U + FFFF (questo vale per le versioni di Windows NT precedenti al 2000).
- Una sequenza come (UTF-8 surrogato alto, seguito da surrogato basso) non è UTF-8 valido, ma invece CESU-8 - risulta da una traduzione ingenua, quindi impropria da UTF-16 a UTF-8: invece di (riconoscimento e ) traducendo una sequenza UTF-16 a 4 byte (che codifica un punto di codice) in una sequenza UTF-8 a 4 byte e vengono tradotti sempre 2 byte, trasformando 2x2 byte in una sequenza UTF-8 a 6 byte non valida.
#$ED#$A0#$BD#$ED#$B8#$85
La conversione della tua sequenza UTF-8 valida nella sequenza UTF-16 valida per me funziona. Ovviamente, assicurati di utilizzare un font appropriato che sia effettivamente in grado di riprodurre gli Emoji:#$F0#$9F#$98#$85#$3d#$d8#$05#$de
// const CP_UTF8= 65001;
function Utf8ToUtf16( const sIn: AnsiString; iSrcCodePage: DWord= CP_UTF8 ): WideString;
var
iLenDest, iLenSrc: Integer;
begin
// First calculate how much space is needed
iLenSrc:= Length( sIn );
iLenDest:= MultiByteToWideChar( iSrcCodePage, 0, PAnsiChar(sIn), iLenSrc, nil, 0 );
// Now provide the accurate space
SetLength( result, iLenDest );
if iLenDest> 0 then begin // Otherwise ERROR_INVALID_PARAMETER might occur
if MultiByteToWideChar( iSrcCodePage, 0, PAnsiChar(sIn), iLenSrc, PWideChar(result), iLenDest )= 0 then begin
// GetLastError();
result:= '';
end;
end;
end;
...
Edit1.Font.Name:= 'Segoe UI Symbol'; // Already available in Win7
Edit1.Text:= Utf8ToUtf16( AnsiString(#$F0#$9F#$98#$85' vs. '#$ED#$A0#$BD#$ED#$B8#$85) );
// Should display: 😅 vs. ����
Per quanto ne so Windows non ha né una tabella codici per CESU-8, né per WTF-8 e come tale non si occuperà del tuo UTF-8 non valido. Anche l'uso di MB_PRECOMPOSEDè sconsigliato e non si applica comunque a questo caso.
Parla con chi ti dà UTF-8 non valido e chiedi di fare il suo lavoro corretto (o di darti l'UTF-16 subito). Altrimenti è necessario pre-elaborare l'UTF-8 in arrivo scansionandolo per la corrispondenza delle coppie surrogate per poi sostituire quei byte in una sequenza corretta. Non impossibile, nemmeno così difficile, ma un noioso lavoro di pazienza.
#$ED#$A0#$BDè la forma codificata UTF-8 del punto di codice Unicode U+D83D, che è un surrogato alto .
#$ED#$B8#$85è la forma codificata UTF-8 del punto di codice Unicode U+DE05, che è un surrogato basso .
#$F0#$9F#$98#$85è la forma codificata UTF-8 del punto di codice Unicode U+1F605.
I punti di codice Unicode nell'intervallo surrogato sono riservati per UTF-16 e illegali da usare da soli, motivo per cui vengono visualizzati �quando vengono stampati.
Questi surrogati sono i surrogati UTF-16 appropriati per il punto di codice Unicode U + 1F605 ( 😅).
Quindi, quello che hai è un problema di doppia codifica che deve essere risolto all'origine in cui vengono generati i dati UTF-8. U+1F605viene prima codificato in UTF-16, non UTF-8, quindi i suoi surrogati vengono maltrattati come punti di codice Unicode e codificati individualmente in UTF-8. Quello che vuoi invece è che codepoint U+1F605sia codificato così com'è direttamente in UTF-8.
Se non riesci a correggere l'origine dei dati UTF-8, dovrai solo rilevare manualmente questa codifica errata e gestire i dati come UTF-16. Decodificare i dati UTF-8 in UTF-32 e, se il risultato contiene punti di codice surrogati, creare una stringa UTF-16 separata della stessa lunghezza e copiare i punti di codice così come sono in quella stringa, troncando i loro valori a 16 bit. Quindi puoi usare quella stringa UTF-16 secondo necessità. Altrimenti, se non sono presenti surrogati, è possibile decodificare l'UTF-8 direttamente in una stringa UTF-16 normalmente e utilizzare invece quel risultato.
AGGIORNAMENTO : come menzionato nella risposta di @ AmigoJack, questi dati utilizzano la codifica CESU-8 (è documentato nell'interfaccia sorgente?). Quindi, sapendo questo ora, puoi semplicemente rinunciare al rilevamento manuale e presumere che tutti i dati UTF-8 da questa fonte siano CESU-8 e decodificarli manualmente come ho descritto sopra (né MultiByteToWideChar()né il Delphi RTL sarà in grado di gestirlo automaticamente per tu), almeno fino a quando l'interfaccia non viene corretta, ad esempio:
function UTF8DenormalizedToString(s: PAnsiChar): UnicodeString;
var
utf32: UCS4String;
len, i: Integer;
begin
utf32 := ... decode utf8 to utf32 ...; // I leave this as an exercise for you!
len := Length(utf32) - 1; // UCS4String includes a null terminator
SetLength(Result, len);
for i := 1 to len do
Result[i] := WideChar(utf32[i-1] and $FFFF); // UCS4String is 0-indexed
end;