Conversione di caratteri denormalizzati con UTF8String

Aug 25 2020

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

2 SalvadorDíazFau Aug 26 2020 at 23:09

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;
2 AmigoJack Aug 25 2020 at 23:27
  • 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.

2 RemyLebeau Aug 25 2020 at 23:25

#$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;