Conversion de caractères dénormalisés avec UTF8String

Aug 25 2020

Lors de la conversion d'emoji encodés en UTF-8 en chaîne, nous n'avons pas obtenu les caractères corrects en utilisant UTF8ToString. Nous recevons ces caractères UTF8 d'une interface externe. Nous avons testé les caractères UTF avec un décodeur UTF8 en ligne et avons vu qu'ils contiennent les caractères corrects. Je soupçonne que ce sont des personnages composites.

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;

Sortie dans Memo1:

Dénormalisé:

Normalisé: 😅😆😊

L'écriture de la propre fonction de conversion basée sur la fonction WinApi MultiByteToWideCharn'a pas résolu ce problème.

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;

Réponses

2 SalvadorDíazFau Aug 26 2020 at 23:09

Si vous avez des données CESU-8 dans une mémoire tampon et que vous devez les convertir en UTF-8, vous pouvez remplacer les paires de substitution par un seul caractère encodé en UTF-8. Le reste des données peut rester inchangé.

Dans ce cas, votre emoji est le suivant:

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

Le substitut élevé dans CESU-8 a ces données: 003D $

Et le substitut faible dans CESU-8 a ces données: 0205 $

Comme Rémy et AmigoJack l'ont souligné, vous trouverez ces valeurs lorsque vous décodez la version UTF-16 de l'emoji.

Dans le cas de l'UTF-16, vous devrez également multiplier le $003D value by $400 (shl 10), ajoutez le résultat à $0205 and then add $10000 au résultat final pour obtenir le point de code.

Une fois que vous avez le point de code, vous pouvez le convertir en un ensemble de valeurs UTF-8 de 4 octets.

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 se compose de 1, 2, 3 ou 4 octets par caractère. Le point de code U + 1F605 est correctement codé comme .#$F0#$9F#$98#$85
  • UTF-16 se compose de 2 ou 4 octets par caractère. Les séquences de 4 octets sont nécessaires pour coder des points de code au-delà de U + FFFF (comme la plupart des Emojis). Seul UCS-2 est limité aux points de code U + 0000 à U + FFFF (cela s'applique aux versions de Windows NT antérieures à 2000).
  • Une séquence comme (UTF-8 high substitut, suivi par low substitate) n'est pas un UTF-8 valide, mais plutôt CESU-8 - il résulte d'une traduction naïve, donc incorrecte de UTF-16 à UTF-8: au lieu de (reconnaître et ) traduisant une séquence UTF-16 de 4 octets (codant un point de code) en une séquence UTF-8 de 4 octets uniquement et toujours 2 octets sont traduits, transformant 2x2 octets en une séquence UTF-8 non valide de 6 octets.#$ED#$A0#$BD#$ED#$B8#$85

La conversion de votre séquence UTF-8 valide en séquence UTF-16 valide fonctionne pour moi. Bien sûr, assurez-vous d'utiliser une police appropriée qui est réellement capable de rendre les 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. ����

À ma connaissance, Windows n'a pas de page de code pour CESU-8, ni pour WTF-8 et, en tant que tel, ne traitera pas votre UTF-8 invalide. De plus, l'utilisation de MB_PRECOMPOSEDest déconseillée et ne s'applique de toute façon pas à ce cas.

Parlez à celui qui vous donne un UTF-8 invalide et demandez-lui de corriger son travail (ou de vous donner immédiatement l'UTF-16). Sinon, vous devez prétraiter l'UTF-8 entrant en le scannant pour rechercher des paires de substitution correspondantes pour ensuite remplacer ces octets dans une séquence appropriée. Pas impossible, même pas si difficile, mais un travail ennuyeux de patience.

2 RemyLebeau Aug 25 2020 at 23:25

#$ED#$A0#$BDest la forme codée UTF-8 du point de code Unicode U+D83D, qui est un substitut élevé .

#$ED#$B8#$85est la forme codée UTF-8 du point de code Unicode U+DE05, qui est un substitut faible .

#$F0#$9F#$98#$85est la forme codée UTF-8 du point de code Unicode U+1F605.

Unicode codepoints dans la gamme de substitution sont réservés pour UTF-16 et illégale à utiliser sur leur propre, ce qui est la raison pour laquelle vous voyez lors de l' impression.

Ces substituts se trouvent être les substituts UTF-16 appropriés pour le point de code Unicode U + 1F605 ( 😅).

Donc, ce que vous avez, c'est un problème de double codage qui doit être résolu à la source où les données UTF-8 sont générées. U+1F605est d'abord codé en UTF-16, pas en UTF-8, puis ses substituts sont maltraités en tant que points de code Unicode et codés individuellement en UTF-8. Ce que vous voulez à la place, c'est que le point de code U+1F605soit encodé tel quel directement en UTF-8.

Si vous ne pouvez pas réparer la source des données UTF-8, il vous suffira de détecter manuellement cet encodage malformé et de traiter les données comme UTF-16 à la place. Décodez les données UTF-8 en UTF-32, et si le résultat contient des points de code de substitution, créez une chaîne UTF-16 distincte de la même longueur et copiez les points de code tels quels dans cette chaîne, en tronquant leurs valeurs à 16 bits. Ensuite, vous pouvez utiliser cette chaîne UTF-16 selon vos besoins. Sinon, si aucun substitut n'est présent, vous pouvez décoder l'UTF-8 directement en une chaîne UTF-16 normalement et utiliser ce résultat à la place.

MISE À JOUR : comme mentionné dans la réponse de @ AmigoJack, ces données utilisent l'encodage CESU-8 (est-ce documenté dans l'interface source?). Donc, sachant cela maintenant, vous pouvez simplement renoncer à la détection manuelle et supposer que toutes les données UTF-8 de cette source sont CESU-8 et les décoder manuellement comme je l'ai décrit ci-dessus ( MultiByteToWideChar()ni le Delphi RTL ne sera en mesure de le gérer automatiquement pour vous), au moins jusqu'à ce que l'interface soit corrigée, par exemple:

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;