使用 UTF8String 转换非规范化字符
Converting Denormalized Characters with UTF8String
将以 UTF-8 编码的表情符号转换为字符串时,我们无法使用 UTF8ToString 获得正确的字符。我们从外部接口接收这些 UTF8 字符。
我们使用在线 UTF8 解码器测试了 UTF 字符,发现它们包含正确的字符。我怀疑这些是复合字符。
procedure TestUTF8Convertion;
const
utf8Denormalized: RawByteString = #$ED#$A0#$BD#$ED#$B8## + #$ED#$A0#$BD#$ED#$B8## + #$ED#$A0#$BD#$ED#$B8#A;
utf8Normalized: RawByteString = #$F0#F## + #$F0#F## + #$F0#F##A;
begin
Memo1.Lines.Add(UTF8ToString(utf8Denormalized));
Memo1.Lines.Add(UTF8ToString(utf8Normalized));
end;
Memo1 中的输出:
非规范化:���� ���� ����
归一化:
基于WinApi函数写自己的转换函数MultiByteToWideChar
没有解决这个问题
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;
#$ED#$A0#$BD
是 Unicode 代码点 U+D83D
的 UTF-8 编码形式,它是 high surrogate.
#$ED#$B8#
是 Unicode 代码点 U+DE05
的 UTF-8 编码形式,它是 low surrogate.
#$F0#F##
是 Unicode 代码点 U+1F605
.
的 UTF-8 编码形式
代理项范围内的 Unicode 代码点 保留 用于 UTF-16 并且 非法 单独使用,这就是您看到的原因�
打印时。
这些代理恰好是 Unicode 代码点 U+1F605 (</code>) 的正确 UTF-16 代理。</p>
<p>因此,您遇到的是一个 double-encoding 问题,需要在生成 UTF-8 数据的源中解决。 <code>U+1F605
首先被编码为 UTF-16,而不是 UTF-8,然后它的替代品被 滥用 作为 Unicode 代码点并单独编码为 UTF-8。您想要的是将代码点 U+1F605
直接编码 as-is 为 UTF-8。
如果您无法修复 UTF-8 数据的来源,那么您将不得不手动检测这种格式错误的编码并将数据作为 UTF-16 处理。将 UTF-8 数据解码为 UTF-32,如果结果包含任何代理代码点,则创建一个单独的相同长度的 UTF-16 字符串,并将代码点 as-is 复制到该字符串中,将它们的值截断为 16-少量。然后您可以根据需要使用该 UTF-16 字符串。否则,如果不存在代理项,那么您可以正常地将 UTF-8 直接解码为 UTF-16 字符串并使用该结果。
UPDATE:如@AmigoJack 的回答所述,此数据使用 CESU-8 编码(在源界面中记录了吗?)。所以,现在知道了这一点,您可以放弃手动检测并假设来自该源的所有 UTF-8 数据都是 CESU-8 并按照我上面的描述手动解码(MultiByteToWideChar()
和 Delphi RTL 将能够自动为您处理),至少在接口修复之前,例如:
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;
- UTF-8 每个字符由 1、2、3 或 4 个字节组成。代码点 U+1F605 正确编码为
#$F0#F##
.
- UTF-16 consists of 2 or 4 bytes per character. The 4 byte sequences are needed to encode codepoints beyond U+FFFF (such as most Emojis). Only UCS-2 仅限于代码点 U+0000 到 U+FFFF(这适用于 Windows 2000 年之前的 NT 版本)。
- 像
#$ED#$A0#$BD#$ED#$B8#
(UTF-8 高代理项,后跟低代理项)这样的序列不是有效的 UTF-8,而是 CESU-8 - 它源于天真,因此来自 UTF 的不正确翻译-16 到 UTF-8:而不是(识别和)将 4 字节 UTF-16 序列(编码一个代码点)转换为 4 字节 UTF-8 序列,并且总是转换 2 个字节,将 2x2 字节转换为无效的 6 字节UTF-8 序列。
将有效的 UTF-8 序列 #$F0#F##
转换为有效的 UTF-16 序列 #d#$d8##$de
对我有用。当然,请确保您使用的是能够呈现 Emoji 表情的正确字体:
// 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#F##' vs. '#$ED#$A0#$BD#$ED#$B8#) );
// Should display: vs. ����
据我所知,Windows 既没有 CESU-8 的代码页,也没有 WTF-8 的代码页,因此不会处理您的无效 UTF-8。此外,不鼓励使用 MB_PRECOMPOSED
并且不适用于这种情况。
与给你无效 UTF-8 的人交谈,并要求让他的工作正确(或立即给你 UTF-16)。否则,您必须 pre-process 通过扫描传入的 UTF-8 以查找匹配的代理对,然后将这些字节替换为正确的序列。并非不可能,甚至没有那么困难,而是耐心的枯燥工作。
如果缓冲区中有 CESU-8 数据并且需要将其转换为 UTF-8,则可以将代理对替换为单个 UTF-8 编码字符。其余数据可以保持不变。
在这种情况下,您的表情符号是这样的:
- 代码点:01 F6 05
- UTF-8 : F0 9F 98 85
- UTF-16 : D8 3D DE 05
- CESU-8:ED A0 BD ED B8 85
CESU-8 中的高代理有此数据:$003D
而 CESU-8 中的低代理有此数据:$0205
正如 Remy 和 AmigoJack 指出的那样,当您解码表情符号的 UTF-16 版本时,您会发现这些值。
对于 UTF-16,您还需要将 $003D 值乘以 $400 (shl 10),将结果与 $0205 相加,然后将 $10000 与最终结果相加以获得代码点。
获得代码点后,您可以将其转换为一组 4 字节的 UTF-8 值。
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 ) = ) 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 ) = ) 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 [=10=]F;
b := ord(aBuffer[i + 2]) and F;
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 + 000;
TempBuffer[j] := AnsiChar($F0 + ((TempCodePoint and C0000) shr 18));
TempBuffer[j + 1] := AnsiChar( + ((TempCodePoint and F000) shr 12));
TempBuffer[j + 2] := AnsiChar( + ((TempCodePoint and $FC0) shr 6));
TempBuffer[j + 3] := AnsiChar( + (TempCodePoint and F));
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 编码的表情符号转换为字符串时,我们无法使用 UTF8ToString 获得正确的字符。我们从外部接口接收这些 UTF8 字符。 我们使用在线 UTF8 解码器测试了 UTF 字符,发现它们包含正确的字符。我怀疑这些是复合字符。
procedure TestUTF8Convertion;
const
utf8Denormalized: RawByteString = #$ED#$A0#$BD#$ED#$B8## + #$ED#$A0#$BD#$ED#$B8## + #$ED#$A0#$BD#$ED#$B8#A;
utf8Normalized: RawByteString = #$F0#F## + #$F0#F## + #$F0#F##A;
begin
Memo1.Lines.Add(UTF8ToString(utf8Denormalized));
Memo1.Lines.Add(UTF8ToString(utf8Normalized));
end;
Memo1 中的输出:
非规范化:���� ���� ����
归一化:
基于WinApi函数写自己的转换函数MultiByteToWideChar
没有解决这个问题
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;
#$ED#$A0#$BD
是 Unicode 代码点 U+D83D
的 UTF-8 编码形式,它是 high surrogate.
#$ED#$B8#
是 Unicode 代码点 U+DE05
的 UTF-8 编码形式,它是 low surrogate.
#$F0#F##
是 Unicode 代码点 U+1F605
.
代理项范围内的 Unicode 代码点 保留 用于 UTF-16 并且 非法 单独使用,这就是您看到的原因�
打印时。
这些代理恰好是 Unicode 代码点 U+1F605 (</code>) 的正确 UTF-16 代理。</p>
<p>因此,您遇到的是一个 double-encoding 问题,需要在生成 UTF-8 数据的源中解决。 <code>U+1F605
首先被编码为 UTF-16,而不是 UTF-8,然后它的替代品被 滥用 作为 Unicode 代码点并单独编码为 UTF-8。您想要的是将代码点 U+1F605
直接编码 as-is 为 UTF-8。
如果您无法修复 UTF-8 数据的来源,那么您将不得不手动检测这种格式错误的编码并将数据作为 UTF-16 处理。将 UTF-8 数据解码为 UTF-32,如果结果包含任何代理代码点,则创建一个单独的相同长度的 UTF-16 字符串,并将代码点 as-is 复制到该字符串中,将它们的值截断为 16-少量。然后您可以根据需要使用该 UTF-16 字符串。否则,如果不存在代理项,那么您可以正常地将 UTF-8 直接解码为 UTF-16 字符串并使用该结果。
UPDATE:如@AmigoJack 的回答所述,此数据使用 CESU-8 编码(在源界面中记录了吗?)。所以,现在知道了这一点,您可以放弃手动检测并假设来自该源的所有 UTF-8 数据都是 CESU-8 并按照我上面的描述手动解码(MultiByteToWideChar()
和 Delphi RTL 将能够自动为您处理),至少在接口修复之前,例如:
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;
- UTF-8 每个字符由 1、2、3 或 4 个字节组成。代码点 U+1F605 正确编码为
#$F0#F##
. - UTF-16 consists of 2 or 4 bytes per character. The 4 byte sequences are needed to encode codepoints beyond U+FFFF (such as most Emojis). Only UCS-2 仅限于代码点 U+0000 到 U+FFFF(这适用于 Windows 2000 年之前的 NT 版本)。
- 像
#$ED#$A0#$BD#$ED#$B8#
(UTF-8 高代理项,后跟低代理项)这样的序列不是有效的 UTF-8,而是 CESU-8 - 它源于天真,因此来自 UTF 的不正确翻译-16 到 UTF-8:而不是(识别和)将 4 字节 UTF-16 序列(编码一个代码点)转换为 4 字节 UTF-8 序列,并且总是转换 2 个字节,将 2x2 字节转换为无效的 6 字节UTF-8 序列。
将有效的 UTF-8 序列 #$F0#F##
转换为有效的 UTF-16 序列 #d#$d8##$de
对我有用。当然,请确保您使用的是能够呈现 Emoji 表情的正确字体:
// 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#F##' vs. '#$ED#$A0#$BD#$ED#$B8#) );
// Should display: vs. ����
据我所知,Windows 既没有 CESU-8 的代码页,也没有 WTF-8 的代码页,因此不会处理您的无效 UTF-8。此外,不鼓励使用 MB_PRECOMPOSED
并且不适用于这种情况。
与给你无效 UTF-8 的人交谈,并要求让他的工作正确(或立即给你 UTF-16)。否则,您必须 pre-process 通过扫描传入的 UTF-8 以查找匹配的代理对,然后将这些字节替换为正确的序列。并非不可能,甚至没有那么困难,而是耐心的枯燥工作。
如果缓冲区中有 CESU-8 数据并且需要将其转换为 UTF-8,则可以将代理对替换为单个 UTF-8 编码字符。其余数据可以保持不变。
在这种情况下,您的表情符号是这样的:
- 代码点:01 F6 05
- UTF-8 : F0 9F 98 85
- UTF-16 : D8 3D DE 05
- CESU-8:ED A0 BD ED B8 85
CESU-8 中的高代理有此数据:$003D
而 CESU-8 中的低代理有此数据:$0205
正如 Remy 和 AmigoJack 指出的那样,当您解码表情符号的 UTF-16 版本时,您会发现这些值。
对于 UTF-16,您还需要将 $003D 值乘以 $400 (shl 10),将结果与 $0205 相加,然后将 $10000 与最终结果相加以获得代码点。
获得代码点后,您可以将其转换为一组 4 字节的 UTF-8 值。
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 ) = ) 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 ) = ) 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 [=10=]F;
b := ord(aBuffer[i + 2]) and F;
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 + 000;
TempBuffer[j] := AnsiChar($F0 + ((TempCodePoint and C0000) shr 18));
TempBuffer[j + 1] := AnsiChar( + ((TempCodePoint and F000) shr 12));
TempBuffer[j + 2] := AnsiChar( + ((TempCodePoint and $FC0) shr 6));
TempBuffer[j + 3] := AnsiChar( + (TempCodePoint and F));
inc(j, 4);
inc(i, 6);
end
else
begin
TempBuffer[j] := aBuffer[i];
inc(i);
inc(j);
end;
Result := < save the buffer here >;
end;