【问题标题】:Converting Denormalized Characters with UTF8String使用 UTF8String 转换非规范化字符
【发布时间】:2020-12-13 21:44:17
【问题描述】:

将 UTF-8 编码的表情符号转换为字符串时,我们没有使用 UTF8ToString 获得正确的字符。我们从外部接口接收这些 UTF8 字符。 我们使用在线 UTF8 解码器测试了 UTF 字符,发现它们包含正确的字符。我怀疑这些是复合字符。

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;

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 在“非私人使用高代理”范围内,#$ED#$B8#$85 在 UTF-8 中的“低代理”范围内,它本身永远不会有意义。剩下的#$20 只是一个空格。见stackoverflow.com/a/51051607/4299358
  • 我不明白的是:UTF-8 序列#$ED#$A0#$BD#$ED#$B8#$85 都显示了这个字形:�。 (我尝试了以下 UTF8Decoder:mothereff.in/utf-8)而串联序列 \xED\xA0\xBD\xED\xB8\x85 显示预期的表情符号字形:????
  • 回到我的问题:如何转换这个 UTF-8 序列 #$F0#$9F#$98#$85 以便获得表情符号 U+1F605
  • @SchneiderInfosystemsLtd 查看我刚刚发布的答案。

标签: delphi utf-8 delphi-10.4-sydney


【解决方案1】:

如果缓冲区中有 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 $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;

【讨论】:

  • 如果Valid*() 函数看起来太复杂,那么stackoverflow.com/a/34156887 总结得很好。 “纠正”无效的 UTF-8 很容易:只需搜索 #$ed,然后比较以下字节。
【解决方案2】:

#$ED#$A0#$BD 是 Unicode 代码点 U+D83D 的 UTF-8 编码形式,它是一个高代理项

#$ED#$B8#$85 是 Unicode 代码点 U+DE05 的 UTF-8 编码形式,它是一个低代理项

#$F0#$9F#$98#$85 是 Unicode 代码点 U+1F605 的 UTF-8 编码形式。

代理范围内的 Unicode 代码点保留以供 UTF-16 和 非法单独使用,这就是打印时看到 的原因。

这些代理恰好是 Unicode 代码点 U+1F605 (?) 的正确 UTF-16 代理。

因此,您遇到的是一个双重编码问题,需要在生成 UTF-8 数据的源处进行修复。 U+1F605 首先被编码为 UTF-16,而不是 UTF-8,然后它的代理被虐待作为 Unicode 代码点并单独编码为 UTF-8。您想要的是代码点 U+1F605 直接按原样编码为 UTF-8。

如果您无法修复 UTF-8 数据的来源,那么您只需手动检测这种格式错误的编码并将数据作为 UTF-16 处理即可。将 UTF-8 数据解码为 UTF-32,如果结果包含任何代理代码点,则创建一个单独的相同长度的 UTF-16 字符串,并将代码点原样复制到该字符串中,将它们的值截断为 16 位。然后,您可以根据需要使用该 UTF-16 字符串。否则,如果不存在代理项,那么您可以正常将 UTF-8 直接解码为 UTF-16 字符串并使用该结果。

更新:如@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 的源。在显示的解决方案中,我还没有看到如何将 UTF8 解码为 UCS4。我只发现了复杂的 C 解决方案,它们根据 UTF-8 表示法进行这种转换,并具有复杂的大小写区别。我第一次尝试将其转换为帕斯卡没有奏效。没有现成的 Delphi 转换吗?
  • @SchneiderInfosystemsLtd UTF-8 很容易手动解码,StackOverflow 上有很多示例(我自己发布了几个)。只是还没时间在这里写。也许我明天会添加一些东西
【解决方案3】:
  • UTF-8 由每个字符 1、2、3 或 4 个字节组成。代码点 U+1F605 正确编码为 #$F0#$9F#$98#$85
  • UTF-16 由每个字符 2 或 4 个字节组成。需要 4 字节序列来编码 U+FFFF 以外的代码点(例如大多数表情符号)。只有UCS-2 仅限于代码点 U+0000 到 U+FFFF(这适用于 2000 年之前的 Windows NT 版本)。
  • #$ED#$A0#$BD#$ED#$B8#$85(UTF-8 高代理,后跟低代理)这样的序列不是有效的 UTF-8,而是CESU-8 - 它是由幼稚导致的,因此从 UTF-16 到 UTF-8 的转换不正确

将您的有效 UTF-8 序列 #$F0#$9F#$98#$85 转换为有效的 UTF-16 序列 #$3d#$d8#$05#$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#$9F#$98#$85' vs. '#$ED#$A0#$BD#$ED#$B8#$85) );
  // Should display: ? vs. ����

据我所知,Windows 既没有 CESU-8 的代码页,也没有 WTF-8 的代码页,因此不会处理您的无效 UTF-8。同样不鼓励使用MB_PRECOMPOSED,并且无论如何都不适用于这种情况。

与给您无效 UTF-8 的人交谈,并要求纠正他的工作(或立即给您 UTF-16)。否则,您必须通过扫描传入的 UTF-8 以查找匹配的代理对来预处理传入的 UTF-8,然后将这些字节替换为正确的序列。不是不可能,甚至不是那么困难,而是一项枯燥的耐心工作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-27
    • 2023-03-06
    • 1970-01-01
    • 1970-01-01
    • 2013-08-21
    • 2016-05-13
    相关资源
    最近更新 更多