Conversão de caracteres desnormalizados com UTF8String
Ao converter o emoji codificado em UTF-8 em string, não obtivemos os caracteres corretos usando UTF8ToString. Recebemos esses caracteres UTF8 de uma interface externa. Testamos os caracteres UTF com um decodificador UTF8 online e vimos que eles contêm os caracteres corretos. Suspeito que sejam personagens compostos.
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;
Saída em Memo1:
Desnormalizado:
Normalizado: 😅😆😊
Escrever a própria função de conversão baseada na função WinApi MultiByteToWideCharnão resolveu esse 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;
Respostas
Se você tiver dados CESU-8 em um buffer e precisar convertê-los em UTF-8, poderá substituir os pares substitutos por um único caractere codificado em UTF-8. O resto dos dados podem ser deixados inalterados.
Nesse caso, seu emoji é este:
- ponto de código: 01 F6 05
- UTF-8: F0 9F 98 85
- UTF-16: D8 3D DE 05
- CESU-8: ED A0 BD ED B8 85
O substituto alto em CESU-8 tem estes dados: $ 003D
E o substituto baixo no CESU-8 tem estes dados: $ 0205
Como Remy e AmigoJack apontaram, você encontrará esses valores quando decodificar a versão UTF-16 do emoji.
No caso do UTF-16, você também precisará multiplicar o $003D value by $400 (shl 10), adicione o resultado a $0205 and then add $10000 até o resultado final para obter o ponto de código.
Depois de ter o ponto de código, você pode convertê-lo em um conjunto de valores UTF-8 de 4 bytes.
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 consiste em 1, 2, 3 ou 4 bytes por caractere. O codepoint U + 1F605 está codificado corretamente como .
#$F0#$9F#$98#$85 - UTF-16 consiste em 2 ou 4 bytes por caractere. As sequências de 4 bytes são necessárias para codificar pontos de código além de U + FFFF (como a maioria dos Emojis). Apenas UCS-2 é limitado a pontos de código U + 0000 a U + FFFF (isso se aplica a versões do Windows NT anteriores a 2000).
- Uma sequência como (UTF-8 substituto alto, seguido por substituto baixo) não é UTF-8 válido, mas sim CESU-8 - resulta de ingênuo, portanto tradução imprópria de UTF-16 para UTF-8: em vez de (reconhecendo e ) traduzindo uma sequência UTF-16 de 4 bytes (codificando um ponto de código) em uma sequência UTF-8 de 4 bytes apenas e sempre 2 bytes são traduzidos, transformando 2x2 bytes em uma sequência UTF-8 de 6 bytes inválida.
#$ED#$A0#$BD#$ED#$B8#$85
Converter sua sequência UTF-8 válida em uma sequência UTF-16 válida funciona para mim. Claro, certifique-se de usar uma fonte adequada que seja realmente capaz de renderizar Emojis:#$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. ����
Que eu saiba, o Windows não tem uma página de códigos para CESU-8, nem para WTF-8 e, como tal, não lida com seu UTF-8 inválido. Além disso, o uso de MB_PRECOMPOSEDé desencorajado e não se aplica a este caso.
Fale com quem lhe der UTF-8 inválido e exija que o trabalho dele seja corrigido (ou lhe entregue o UTF-16 imediatamente). Caso contrário, você deve pré-processar o UTF-8 de entrada examinando-o em busca de pares substitutos correspondentes para, então, substituir esses bytes em uma sequência apropriada. Não impossível, nem tão difícil, mas um trabalho enfadonho de paciência.
#$ED#$A0#$BDé a forma codificada em UTF-8 de ponto de código Unicode U+D83D, que é um substituto alto .
#$ED#$B8#$85é a forma codificada em UTF-8 de ponto de código Unicode U+DE05, que é um substituto baixo .
#$F0#$9F#$98#$85é a forma codificada UTF-8 de ponto de código Unicode U+1F605.
Os pontos de código Unicode no intervalo substituto são reservados para UTF-16 e são ilegais para uso por conta própria, razão pela qual você vê �quando impresso.
Esses substitutos são os substitutos UTF-16 adequados para o ponto de código Unicode U + 1F605 ( 😅).
Portanto, o que você tem é um problema de codificação dupla que precisa ser corrigido na fonte onde os dados UTF-8 estão sendo gerados. U+1F605está sendo codificado primeiro para UTF-16, não UTF-8, e então seus substitutos estão sendo maltratados como pontos de código Unicode e codificados individualmente para UTF-8. Em vez disso, o que você deseja é que o codepoint U+1F605seja codificado no estado em que se encontra diretamente para UTF-8.
Se você não conseguir corrigir a origem dos dados UTF-8, terá apenas que detectar manualmente essa codificação malformada e tratar os dados como UTF-16. Decodifique os dados UTF-8 para UTF-32 e, se o resultado contiver quaisquer pontos de código substitutos, crie uma string UTF-16 separada do mesmo comprimento e copie os pontos de código como estão nessa string, truncando seus valores para 16 bits. Então você pode usar essa string UTF-16 conforme necessário. Caso contrário, se nenhum substituto estiver presente, você pode decodificar o UTF-8 diretamente para uma string UTF-16 normalmente e usar esse resultado.
ATUALIZAÇÃO : conforme mencionado na resposta de @AmigoJack, esses dados estão usando a codificação CESU-8 (isso está documentado na interface de origem?). Portanto, sabendo disso agora, você pode simplesmente renunciar à detecção manual e assumir que todos os dados UTF-8 desta fonte são CESU-8 e decodificá-los manualmente como descrevi acima (nem MultiByteToWideChar()nem o Delphi RTL será capaz de manipulá-los automaticamente para você), pelo menos até que a interface seja corrigida, por exemplo:
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;