web-dev-qa-db-fra.com

Conversion d'UnicodeString en AnsiString

Dans les temps anciens, j'avais une fonction qui convertissait un WideString en AnsiString de la page de code spécifiée:

function WideStringToString(const Source: WideString; CodePage: UINT): AnsiString;
...
begin
   ...
    // Convert source UTF-16 string (WideString) to the destination using the code-page
    strLen := WideCharToMultiByte(CodePage, 0,
        PWideChar(Source), Length(Source), //Source
        PAnsiChar(cpStr), strLen, //Destination
        nil, nil);
    ...
end;

Et tout a fonctionné. J'ai passé la fonction une chaîne unicode (c'est-à-dire des données encodées UTF-16) et je l'ai convertie en AnsiString, étant entendu que les octets dans le AnsiString caractères représentés de la page de codes spécifiée.

Par exemple:

TUnicodeHelper.WideStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 1252);

retournerait la chaîne encodée Windows-1252:

The qùíçk brown fôx jumped ovêr the lázÿ dog

Remarque: Des informations ont bien sûr été perdues lors de la conversion du jeu de caractères Unicode complet aux limites limitées de la page de codes Windows-1252:

  • Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ (avant)
  • The qùíçk brown fôx jumped ovêr the lázÿ dog (après)

Mais Windows WideChartoMultiByte fait un très bon travail de mappage optimal; comme il est conçu pour le faire.

Maintenant les temps après

Maintenant, nous sommes dans l'après-temps. WideString est maintenant un paria, avec UnicodeString étant la bonté. C'est un changement sans conséquence; car la fonction Windows n'avait besoin que d'un pointeur vers une série de WideChar de toute façon (qui est également un UnicodeString). Nous changeons donc la déclaration pour utiliser UnicodeString à la place:

funtion WideStringToString(const Source: UnicodeString; CodePage: UINT): AnsiString;
begin
   ...
end;

Nous arrivons maintenant à la valeur de retour. j'ai un AnsiString qui contient les octets:

54 68 65 20 71 F9 ED E7  The qùíç
6B 20 62 72 6F 77 6E 20  k brown 
66 F4 78 20 6A 75 6D 70  fôx jump
65 64 20 6F 76 EA 72 20  ed ovêr 
74 68 65 20 6C E1 7A FF  the lázÿ
20 64 6F 67               dog

Autrefois, c'était bien. J'ai gardé une trace de la page de code que le AnsiString contenait réellement; je devais rappelez-vous que le AnsiString retourné n'était pas encodé à l'aide des paramètres régionaux de l'ordinateur (par exemple Windows 1258), mais à la place est encodé à l'aide d'une autre page de code (le CodePage page de codes).

Mais dans Delphi XE6, un AnsiString contient également secrètement la page de code:

  • codePage: 1258
  • longueur: 44
  • valeur: The qùíçk brown fôx jumped ovêr the lázÿ dog

Cette page de codes est incorrecte. Delphi spécifie la page de codes de mon ordinateur, plutôt que la page de codes de la chaîne. Techniquement, ce n'est pas un problème, j'ai toujours compris que le AnsiString était dans une page de code particulière, je devais juste être sûr de transmettre cette information.

Donc, quand je voulais décoder la chaîne, je devais passer la page de code avec:

s := TUnicodeHeper.StringToWideString(s, 1252);

avec

function StringToWideString(s: AnsiString; CodePage: UINT): UnicodeString;
begin
   ...
   MultiByteToWideChar(...);
   ...
end;

Puis une personne fout tout

Le problème était que dans les temps anciens, j'ai déclaré un type appelé Utf8String:

type
   Utf8String = type AnsiString;

Parce qu'il était assez courant d'avoir:

function TUnicodeHelper.WideStringToUtf8(const s: UnicodeString): Utf8String;
begin
   Result := WideStringToString(s, CP_UTF8);
end;

et l'inverse:

function TUnicodeHelper.Utf8ToWideString(const s: Utf8String): UnicodeString;
begin
   Result := StringToWideString(s, CP_UTF8);
end;

Maintenant, dans XE6, j'ai une fonction qui prend un Utf8String. Si du code existant quelque part prenait un AnsiString encodé en UTF-8 et essayait de le convertir en UnicodeString en utilisant Utf8ToWideString, Il échouerait:

s: AnsiString;
s := UnicodeStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', CP_UTF8);

...

 ws: UnicodeString;
 ws := Utf8ToWideString(s); //Delphi will treat s an CP1252, and convert it to UTF8

Ou pire, est l'étendue du code existant qui fait:

s: Utf8String;
s := UnicodeStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', CP_UTF8);

La chaîne retournée deviendra totalement mutilée:

  • la fonction retourne AnsiString(1252) (AnsiString étiquetée comme encodée à l'aide de la page de codes actuelle)
  • le résultat de retour est stocké dans une chaîne AnsiString(65001) (Utf8String)
  • Delphi convertit la chaîne encodée UTF-8 en UTF-8 comme si elle était 1252.

Comment avancer

Idéalement, ma fonction UnicodeStringToString(string, codePage) (qui renvoie un AnsiString) pourrait définir CodePage à l'intérieur de la chaîne pour correspondre à la page de code réelle en utilisant quelque chose comme SetCodePage :

function UnicodeStringToString(s: UnicodeString; CodePage: UINT): AnsiString;
begin
   ...
   WideCharToMultiByte(...);
   ...

   //Adjust the codepage contained in the AnsiString to match reality
   //SetCodePage(Result, CodePage, False); SetCodePage only works on RawByteString
   if Length(Result) > 0 then
      PStrRec(PByte(Result) - SizeOf(StrRec)).codePage := CodePage;
end;

Sauf que le nettoyage manuel avec la structure interne d'un AnsiString est horriblement dangereux.

Alors qu'en est-il du retour de RawByteString?

Il a été dit, à plusieurs reprises, par beaucoup de gens qui ne sont pas moi que RawByteString est censé être le destinataire universel; ce n'était pas censé être un paramètre de retour:

function UnicodeStringToString(s: UnicodeString; CodePage: UINT): RawByteString;
begin
   ...
   WideCharToMultiByte(...);
   ...

   //Adjust the codepage contained in the AnsiString to match reality
   SetCodePage(Result, CodePage, False); SetCodePage only works on RawByteString
end;

Cela a l'avantage de pouvoir utiliser le SetCodePage supporté et documenté.

Mais si nous allons franchir une ligne et commencer à renvoyer RawByteString, Delphi a sûrement déjà une fonction qui peut convertir une UnicodeString en une chaîne de RawByteString et vice versa:

function WideStringToString(const s: UnicodeString; CodePage: UINT): RawByteString;
begin
   Result := SysUtils.Something(s, CodePage);
end;

function StringToWideString(const s: RawByteString; CodePage: UINT): UnicodeString;
begin
   Result := SysUtils.SomethingElse(s, CodePage);       
end;

Mais qu'est-ce que c'est?

Ou que dois-je faire d'autre?

Ce fut un ensemble de fond de longue haleine pour une question triviale. La question réelle est, bien sûr, que dois-je faire à la place? Il y a beaucoup de code qui dépend du UnicodeStringToString et de l'inverse.

tl; dr:

Je peux convertir un UnicodeString en UTF en faisant:

Utf8Encode('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ');

et je peux convertir un UnicodeString vers la page de codes actuelle en utilisant:

AnsiString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ');

Mais comment convertir un UnicodeString en une page de code arbitraire (non spécifiée)?

Mon sentiment est que puisque tout est vraiment un AnsiString:

Utf8String = AnsiString(65001);
RawByteString = AnsiString(65535);

je devrais mordre la balle, ouvrir la structure AnsiString et y insérer la page de code correcte:

function StringToAnsi(const s: UnicodeString; CodePage: UINT): AnsiString;
begin
   LocaleCharsFromUnicode(CodePage, ..., s, ...);

   ...

   if Length(Result) > 0 then
      PStrRec(PByte(Result) - SizeOf(StrRec)).codePage := CodePage;
end;

Ensuite, le reste de la VCL tombera en ligne.

22
Ian Boyd

Dans ce cas particulier, l'utilisation de RawByteString est une solution appropriée:

function WideStringToString(const Source: UnicodeString; CodePage: UINT): RawByteString;
var
  strLen: Integer;
begin
  strLen := LocaleCharsFromUnicode(CodePage, 0, PWideChar(Source), Length(Source), nil, 0, nil, nil));
  if strLen > 0 then
  begin
    SetLength(Result, strLen);
    LocaleCharsFromUnicode(CodePage, 0, PWideChar(Source), Length(Source), PAnsiChar(Result), strLen, nil, nil));
    SetCodePage(Result, CodePage, False);
  end;
end;

De cette façon, le RawByteString contient la page de code et l'attribution du RawByteString à tout autre type de chaîne, que ce soit AnsiString ou UTF8String Ou autre, permettra la RTL pour convertir automatiquement les données RawByteString de sa page de codes actuelle vers la page de codes de la chaîne de destination (qui inclut les conversions en UnicodeString).

Si vous devez absolument renvoyer un AnsiString (ce que je ne recommande pas), vous pouvez toujours utiliser SetCodePage() via un transtypage:

function WideStringToString(const Source: UnicodeString; CodePage: UINT): AnsiString;
var
  strLen: Integer;
begin
  strLen := LocaleCharsFromUnicode(CodePage, 0, PWideChar(Source), Length(Source), nil, 0, nil, nil));
  if strLen > 0 then
  begin
    SetLength(Result, strLen);
    LocaleCharsFromUnicode(CodePage, 0, PWideChar(Source), Length(Source), PAnsiChar(Result), strLen, nil, nil));
    SetCodePage(PRawByteString(@Result)^, CodePage, False);
  end;
end;

L'inverse est beaucoup plus facile, utilisez simplement la page de code déjà stockée dans un (Ansi|RawByte)String (Assurez-vous simplement que ces pages de code sont toujours précises), car la RTL sait déjà comment récupérer et utiliser la page de code pour vous:

function StringToWideString(const Source: AnsiString): UnicodeString;
begin
  Result := UnicodeString(Source);
end;
function StringToWideString(const Source: RawByteString): UnicodeString;
begin
  Result := UnicodeString(Source);
end;

Cela étant dit, je suggérerais de supprimer complètement les fonctions d'assistance et d'utiliser simplement des chaînes tapées à la place. Laissez le RTL gérer les conversions pour vous:

type
  Win1252String = type AnsiString(1252);

var
  s: UnicodeString;
  a: Win1252String;
begin
  s := 'Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ';
  a := Win1252String(s);
  s := UnicodeString(a);
end;
var
  s: UnicodeString;
  u: UTF8String;
begin
  s := 'Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ';
  u := UTF8String(s);
  s := UnicodeString(u);
end;
16
Remy Lebeau

Je pense que retourner un RawByteString est probablement aussi bon que vous en aurez. Vous pouvez le faire en utilisant AnsiString comme vous l'avez indiqué, mais RawByteString capture mieux l'intention. Dans ce scénario, un RawByteString compte moralement comme un paramètre au sens des conseils officiels d'Embarcadero. C'est juste une sortie plutôt qu'une entrée. La vraie clé est de ne pas l'utiliser comme variable.

Vous pouvez le coder comme ceci:

function MBCSString(const s: UnicodeString; CodePage: Word): RawByteString;
var
  enc: TEncoding;
  bytes: TBytes;
begin
  enc := TEncoding.GetEncoding(CodePage);
  try
    bytes := enc.GetBytes(s);
    SetLength(Result, Length(bytes));
    Move(Pointer(bytes)^, Pointer(Result)^, Length(bytes));
    SetCodePage(Result, CodePage, False);
  finally
    enc.Free;
  end;
end;

Alors

var
  s: AnsiString;
....
s := MBCSString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 1252);
Writeln(StringCodePage(s));
s := MBCSString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 1251);
Writeln(StringCodePage(s));
s := MBCSString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 65001);
Writeln(StringCodePage(s));

sorties 1252, 1251, puis 65001 comme vous vous en doutez.

Et vous pouvez utiliser LocaleCharsFromUnicode si vous préférez. Bien sûr, vous devez prendre sa documentation avec une pincée de sel: LocaleCharsFromUnicode est un wrapper pour la fonction WideCharToMultiByte . Étonnant que le texte ait été écrit depuis que LocaleCharsFromUnicode n'existe sûrement que pour être multi-plateforme.


Cependant, je me demande si vous pouvez faire une erreur en essayant de conserver le texte encodé ANSI dans les variables AnsiString dans votre programme. Normalement, vous seriez encodé en ANSI le plus tard possible (à la limite d'interopérabilité) et décoderiez également le plus tôt possible.

Si vous devez simplement le faire, il existe peut-être une meilleure solution qui évite complètement le redouté AnsiString. Au lieu de stocker le texte dans un AnsiString, stockez-le dans TBytes. Vous disposez déjà de structures de données qui assurent le suivi de l'encodage, alors pourquoi ne pas les conserver. Remplacez l'enregistrement qui contient la page de codes et AnsiString par celui contenant la page de codes et TBytes. Vous ne craindriez alors rien de recoder votre texte derrière votre dos. Et votre code sera prêt à être utilisé sur les compilateurs mobiles.

5
David Heffernan

Se promener à travers System.pas, j'ai trouvé la fonction intégrée SetAnsiString qui fait ce que je veux:

procedure SetAnsiString(Dest: _PAnsiStr; Source: PWideChar; Length: Integer; CodePage: Word);

Il est également important de noter que cette fonction le fait Poussez le CodePage dans la structure StrRec interne pour moi:

PStrRec(PByte(Dest) - SizeOf(StrRec)).codePage := CodePage;

Cela me permet d'écrire quelque chose comme:

function WideStringToString(const s: UnicodeString; DestinationCodePage: Word): AnsiString;
var
   strLen: Integer;
begin
   strLen := Length(Source);

   if strLen = 0 then
   begin
      Result := '';
      Exit;
   end;

   //Delphi XE6 has a function to convert a unicode string to a tagged AnsiString
   SetAnsiString(@Result, @Source[1], strLen, DestinationCodePage);
end;

Alors quand j'appelle:

actual := WideStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 850);

j'obtiens le AnsiString résultant:

codePage: $0352 (850)
elemSize: $0001 (1)
refCnt:   $00000001 (1)
length:   $0000002C (44)
contents: 'The qùíçk brown fôx jumped ovêr the láZÿ dog' 

Un AnsiString avec la page de code appropriée déjà remplie dans le membre secret codePage.

L'autre côté

class function TUnicodeHelper.ByteStringToUnicode(const Source: RawByteString; CodePage: UINT): UnicodeString;
var
    wideLen: Integer;
    dw: DWORD;
begin
{
    See http://msdn.Microsoft.com/en-us/library/dd317756.aspx
    Code Page Identifiers
    for a list of code pages supported in Windows.

    Some common code pages are:
        CP_UTF8 (65001) utf-8               "Unicode (UTF-8)"
        CP_ACP  (0)                         The system default Windows ANSI code page.
        CP_OEMCP    (1)                         The current system OEM code page.
        1252                    Windows-1252    "ANSI Latin 1; Western European (Windows)", this is what most of us in north america use in Windows
        437                 IBM437          "OEM United States", this is your "DOS fonts"
        850                 ibm850          "OEM Multilingual Latin 1; Western European (DOS)", the format accepted by Fincen for LCTR/STR
        28591                   iso-8859-1      "ISO 8859-1 Latin 1; Western European (ISO)", Windows-1252 is a super-set of iso-8859-1, adding things like euro symbol, bullet and ellipses
        20127                   us-ascii            "US-ASCII (7-bit)"
}
    if Length(Source) = 0 then
    begin
        Result := '';
        Exit;
    end;

    // Determine real size of final, string in symbols
//  wideLen := MultiByteToWideChar(CodePage, 0, PAnsiChar(Source), Length(Source), nil, 0);
    wideLen := UnicodeFromLocaleChars(CodePage, 0, PAnsiChar(Source), Length(Source), nil, 0);
    if wideLen = 0 then
    begin
        dw := GetLastError;
        raise EConvertError.Create('[StringToWideString] Could not get wide length of UTF-16 string. Error '+IntToStr(dw)+' ('+SysErrorMessage(dw)+')');
    end;

    // Allocate memory for UTF-16 string
    SetLength(Result, wideLen);

    // Convert source string to UTF-16 (WideString)
//  wideLen := MultiByteToWideChar(CodePage, 0, PAnsiChar(Source), Length(Source), PWChar(wideStr), wideLen);
    wideLen := UnicodeFromLocaleChars(CodePage, 0, PAnsiChar(Source), Length(Source), PWChar(Result), wideLen);
    if wideLen = 0 then
    begin
        dw := GetLastError;
        raise EConvertError.Create('[StringToWideString] Could not convert string to UTF-16. Error '+IntToStr(dw)+' ('+SysErrorMessage(dw)+')');
    end;
end;

Remarque : Tout code publié dans le domaine public. Aucune attribution requise.

3
Ian Boyd