使用 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;