Conversión de caracteres desnormalizados con UTF8String
Al convertir emoji codificados en UTF-8 a cadena, no obtuvimos los caracteres correctos usando UTF8ToString. Recibimos estos caracteres UTF8 de una interfaz externa. Probamos los caracteres UTF con un decodificador UTF8 en línea y vimos que contienen los caracteres correctos. Sospecho que estos son personajes compuestos.
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;
Salida en Memo1:
Desormalizado:
Normalizado: 😅😆😊
Escribir la propia función de conversión basada en la función WinApi MultiByteToWideCharno resolvió este 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;
Respuestas
Si tiene datos CESU-8 en un búfer y necesita convertirlos a UTF-8, puede reemplazar los pares suplentes con un solo carácter codificado en UTF-8. El resto de los datos se pueden dejar sin cambios.
En este caso, tu emoji es este:
- punto 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
El sustituto alto en CESU-8 tiene estos datos: $ 003D
Y el sustituto bajo en CESU-8 tiene este dato: $ 0205
Como señalaron Remy y AmigoJack, encontrará estos valores cuando decodifique la versión UTF-16 del emoji.
En el caso de UTF-16 también necesitará multiplicar el $003D value by $400 (shl 10), agregue el resultado a $0205 and then add $10000 hasta el resultado final para obtener el punto de código.
Una vez que tenga el punto de código, puede convertirlo en un 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 consta de 1, 2, 3 o 4 bytes por carácter. El punto de código U + 1F605 está codificado correctamente como .
#$F0#$9F#$98#$85 - UTF-16 consta de 2 o 4 bytes por carácter. Las secuencias de 4 bytes son necesarias para codificar puntos de código más allá de U + FFFF (como la mayoría de los emojis). Solo UCS-2 está limitado a los puntos de código U + 0000 a U + FFFF (esto se aplica a las versiones de Windows NT anteriores a 2000).
- Una secuencia como (sustituto alto UTF-8, seguido por sustituto bajo) no es UTF-8 válido, sino CESU-8 - resulta de una traducción ingenua, por lo tanto incorrecta de UTF-16 a UTF-8: en lugar de (reconocer y ) traduciendo una secuencia UTF-16 de 4 bytes (codificando un punto de código) en una secuencia UTF-8 de 4 bytes solamente y siempre se traducen 2 bytes, convirtiendo 2x2 bytes en una secuencia UTF-8 de 6 bytes no válida.
#$ED#$A0#$BD#$ED#$B8#$85
Convertir su secuencia UTF-8 válida en la secuencia UTF-16 válida funciona para mí. Por supuesto, asegúrese de usar una fuente adecuada que realmente pueda 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 yo sepa, Windows no tiene una página de códigos para CESU-8, ni para WTF-8 y, como tal, no se ocupará de su UTF-8 no válido. Además, MB_PRECOMPOSEDse desaconseja el uso de y no se aplica a este caso de todos modos.
Habla con quien te dé el UTF-8 inválido y exígele que haga su trabajo correctamente (o que te dé el UTF-16 de inmediato). De lo contrario, debe preprocesar UTF-8 entrante escaneándolo en busca de pares sustitutos coincidentes para luego reemplazar esos bytes en una secuencia adecuada. No es imposible, ni siquiera tan difícil, pero es un aburrido trabajo de paciencia.
#$ED#$A0#$BDes la forma codificada en UTF-8 del punto de código Unicode U+D83D, que es un sustituto alto .
#$ED#$B8#$85es la forma codificada en UTF-8 del punto de código Unicode U+DE05, que es un sustituto bajo .
#$F0#$9F#$98#$85es la forma codificada en UTF-8 del punto de código Unicode U+1F605.
Los puntos de código Unicode en el rango sustituto están reservados para UTF-16 y su uso es ilegal por sí mismos, por lo que se ve �cuando se imprime.
Esos sustitutos resultan ser los sustitutos UTF-16 adecuados para el punto de código Unicode U + 1F605 ( 😅).
Entonces, lo que tiene es un problema de doble codificación que debe solucionarse en la fuente donde se generan los datos UTF-8. U+1F605primero se codifica en UTF-16, no en UTF-8, y luego sus sustitutos se tratan mal como puntos de código Unicode y se codifican individualmente en UTF-8. En cambio, lo que desea es que el punto de código U+1F605se codifique como está directamente en UTF-8.
Si no puede arreglar la fuente de los datos UTF-8, entonces solo tendrá que detectar manualmente esta codificación mal formada y manejar los datos como UTF-16 en su lugar. Decodifique los datos UTF-8 a UTF-32, y si el resultado contiene puntos de código sustitutos, cree una cadena UTF-16 separada de la misma longitud y copie los puntos de código tal cual en esa cadena, truncando sus valores a 16 bits. Entonces puede usar esa cadena UTF-16 según sea necesario. De lo contrario, si no hay sustitutos presentes, puede decodificar el UTF-8 directamente en una cadena UTF-16 normalmente y usar ese resultado en su lugar.
ACTUALIZACIÓN : como se menciona en la respuesta de @ AmigoJack, estos datos utilizan la codificación CESU-8 (¿está documentado en la interfaz de origen?). Entonces, sabiendo esto ahora, puede simplemente renunciar a la detección manual y asumir que todos los datos UTF-8 de esta fuente son CESU-8 y decodificarlos manualmente como describí anteriormente ( MultiByteToWideChar()ni el Delphi RTL podrá manejarlo automáticamente para usted), al menos hasta que se solucione la interfaz, por ejemplo:
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;