Denormalisierte Zeichen mit UTF8String konvertieren

Aug 25 2020

Bei der Konvertierung von in UTF-8 codiertem Emoji in einen String haben wir mit UTF8ToString nicht die richtigen Zeichen erhalten. Wir erhalten diese UTF8-Zeichen von einer externen Schnittstelle. Wir haben die UTF-Zeichen mit einem Online-UTF8-Decoder getestet und festgestellt, dass sie die richtigen Zeichen enthalten. Ich vermute, das sind zusammengesetzte Zeichen.

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;

Ausgabe in Memo1:

Denormalisiert:

Normalisiert: 😅😆😊

Das Schreiben einer eigenen Konvertierungsfunktion basierend auf der WinApi-Funktion MultiByteToWideCharhat dieses Problem nicht gelöst.

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;

Antworten

2 SalvadorDíazFau Aug 26 2020 at 23:09

Wenn Sie CESU-8-Daten in einem Puffer haben und diese in UTF-8 konvertieren müssen, können Sie die Ersatzpaare durch ein einzelnes UTF-8-codiertes Zeichen ersetzen. Der Rest der Daten kann unverändert bleiben.

In diesem Fall lautet Ihr Emoji wie folgt:

  • Codepunkt: 01 F6 05
  • UTF-8: F0 9F 98 85
  • UTF-16: D8 3D DE 05
  • CESU-8: ED A0 BD ED B8 85

Der hohe Ersatz in CESU-8 hat diese Daten: $ 003D

Und der niedrige Ersatz in CESU-8 hat diese Daten: $ 0205

Wie Remy und AmigoJack betonten, werden Sie diese Werte finden, wenn Sie die UTF-16-Version des Emoji dekodieren.

Im Fall von UTF-16 müssen Sie auch die multiplizieren $003D value by $400 (shl 10), addiere das Ergebnis zu $0205 and then add $10000 zum Endergebnis, um den Codepunkt zu erhalten.

Sobald Sie den Codepunkt haben, können Sie ihn in einen 4-Byte-UTF-8-Wertesatz konvertieren.

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;
2 AmigoJack Aug 25 2020 at 23:27
  • UTF-8 besteht aus 1, 2, 3 oder 4 Bytes pro Zeichen. Der Codepunkt U + 1F605 ist korrekt codiert als .#$F0#$9F#$98#$85
  • UTF-16 besteht aus 2 oder 4 Bytes pro Zeichen. Die 4-Byte-Sequenzen werden benötigt, um Codepunkte jenseits von U + FFFF (wie die meisten Emojis) zu codieren. Nur UCS-2 ist auf die Codepunkte U + 0000 bis U + FFFF beschränkt (dies gilt für Windows NT-Versionen vor 2000).
  • Eine Sequenz wie (UTF-8 High Surrogate, gefolgt von Low Surrogate) ist keine gültige UTF-8, sondern CESU-8 - sie resultiert aus naiver, also falscher Übersetzung von UTF-16 nach UTF-8: anstelle von (Erkennen und ) Übersetzen einer 4-Byte-UTF-16-Sequenz (Codierung eines Codepunkts) in eine 4-Byte-UTF-8-Sequenz und immer 2 Bytes werden übersetzt, wodurch 2x2 Bytes in eine ungültige 6-Byte-UTF-8-Sequenz umgewandelt werden.#$ED#$A0#$BD#$ED#$B8#$85

Das Konvertieren Ihrer gültigen UTF-8-Sequenz in die gültige UTF-16-Sequenz funktioniert für mich. Stellen Sie natürlich sicher, dass Sie eine geeignete Schriftart verwenden, mit der Emojis tatsächlich gerendert werden kann:#$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. ����

Meines Wissens hat Windows weder eine Codepage für CESU-8 noch für WTF-8 und wird daher nicht mit Ihrem ungültigen UTF-8 umgehen. Auch von der Verwendung MB_PRECOMPOSEDwird abgeraten und gilt ohnehin nicht für diesen Fall.

Sprechen Sie mit demjenigen, der Ihnen ungültiges UTF-8 gibt, und fordern Sie, seinen Job korrekt zu machen (oder Ihnen das UTF-16 sofort zu geben). Andernfalls müssen Sie eingehendes UTF-8 vorverarbeiten, indem Sie es nach übereinstimmenden Ersatzpaaren durchsuchen, um diese Bytes dann in einer geeigneten Reihenfolge zu ersetzen. Nicht unmöglich, nicht einmal so schwierig, aber eine langweilige Arbeit der Geduld.

2 RemyLebeau Aug 25 2020 at 23:25

#$ED#$A0#$BDist die UTF-8-codierte Form des Unicode-Codepunkts U+D83D, bei dem es sich um einen hohen Ersatz handelt .

#$ED#$B8#$85ist die UTF-8-codierte Form des Unicode-Codepunkts U+DE05, bei dem es sich um einen niedrigen Ersatz handelt .

#$F0#$9F#$98#$85ist die UTF-8-codierte Form des Unicode-Codepunkts U+1F605.

Unicode - Codepoints im Surrogat Bereich sind reserviert für UTF-16 und illegalen Gebrauch auf ihrem eigenen, weshalb man sehen , wenn sie gedruckt.

Diese Surrogate sind zufällig die richtigen UTF-16-Surrogate für den Unicode-Codepunkt U + 1F605 ( 😅).

Sie haben also ein Problem mit der doppelten Codierung, das an der Quelle behoben werden muss, an der die UTF-8-Daten generiert werden. U+1F605wird zuerst in UTF-16 und nicht in UTF-8 codiert, und dann werden seine Ersatzzeichen als Unicode-Codepunkte misshandelt und einzeln in UTF-8 codiert. Stattdessen möchten Sie, dass der Codepoint U+1F605so wie er ist direkt in UTF-8 codiert wird.

Wenn Sie die Quelle der UTF-8-Daten nicht reparieren können, müssen Sie diese fehlerhafte Codierung nur manuell erkennen und die Daten stattdessen als UTF-16 behandeln. Dekodieren Sie die UTF-8-Daten in UTF-32. Wenn das Ergebnis Ersatzcodepunkte enthält, erstellen Sie eine separate UTF-16-Zeichenfolge mit derselben Länge und kopieren Sie die Codepunkte unverändert in diese Zeichenfolge, wobei Sie ihre Werte auf 16 Bit kürzen. Dann können Sie diese UTF-16-Zeichenfolge nach Bedarf verwenden. Andernfalls können Sie, wenn keine Ersatzzeichen vorhanden sind, den UTF-8 direkt direkt in einen UTF-16-String dekodieren und stattdessen dieses Ergebnis verwenden.

UPDATE : Wie in der Antwort von @ AmigoJack erwähnt, verwenden diese Daten die CESU-8-Codierung (ist dies in der Quellschnittstelle dokumentiert?). Wenn Sie dies jetzt wissen, können Sie einfach auf die manuelle Erkennung verzichten und davon ausgehen, dass alle UTF-8-Daten aus dieser Quelle CESU-8 sind, und sie manuell wie oben beschrieben dekodieren (weder MultiByteToWideChar()noch die Delphi RTL können sie automatisch verarbeiten Sie), zumindest bis die Schnittstelle repariert wird, zB:

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;