delphi - Convertir UnicodeString a AnsiString
internationalization delphi-xe6 (3)
En los tiempos antiguos, tenía una función que convertiría una WideString
en una AnsiString
de la página de códigos especificada:
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;
Y todo funcionó. Pasé la función una cadena Unicode (es decir, datos codificados en UTF-16) y la convertí en una AnsiString
, con el entendimiento de que los bytes en la AnsiString
representaban caracteres de la página de códigos especificada.
Por ejemplo:
TUnicodeHelper.WideStringToString(''Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ'', 1252);
devolvería la cadena codificada de Windows-1252
:
The qùíçk brown fôx jumped ovêr the lázÿ dog
Nota: la información, por supuesto, se perdió durante la conversión del conjunto completo de caracteres Unicode a los límites limitados de la página de códigos Windows-1252:
Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ
(antes)The qùíçk brown fôx jumped ovêr the lázÿ dog
(después)
Pero Windows WideChartoMultiByte
hace un muy buen trabajo de mapeo de mejor ajuste; como está diseñado para hacer.
Ahora los tiempos posteriores
Ahora estamos en los tiempos posteriores. WideString
ahora es un paria, siendo UnicodeString
la bondad. Es un cambio intrascendente; como la función de Windows solo necesitaba un puntero a una serie de WideChar
todos modos (que también es un UnicodeString
). Así que cambiamos la declaración para usar UnicodeString
lugar:
funtion WideStringToString(const Source: UnicodeString; CodePage: UINT): AnsiString;
begin
...
end;
Ahora llegamos al valor de retorno. Tengo un AnsiString
que contiene los bytes:
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
En los viejos tiempos eso estaba bien. AnsiString
seguimiento de qué página de AnsiString
contenía realmente el AnsiString
; Tuve que recordar que el AnsiString
devuelto no se codificó con la configuración regional de la computadora (por ejemplo, Windows 1258), sino que se codificó con otra página de códigos (la página de códigos CodePage
).
Pero en Delphi XE6, AnsiString
también contiene en secreto la página de códigos:
- codePage: 1258
- longitud: 44
- valor:
The qùíçk brown fôx jumped ovêr the lázÿ dog
Esta página de códigos es incorrecta. Delphi está especificando la página de códigos de mi computadora, en lugar de la página de códigos que es la cadena. Técnicamente, esto no es un problema, siempre entendí que el AnsiString
estaba en una página de códigos en particular, solo tenía que estar seguro de pasar esa información.
Así que cuando quise descifrar la cadena, tuve que pasar la página de códigos con ella:
s := TUnicodeHeper.StringToWideString(s, 1252);
con
function StringToWideString(s: AnsiString; CodePage: UINT): UnicodeString;
begin
...
MultiByteToWideChar(...);
...
end;
Entonces una persona lo arruina todo
El problema fue que en los tiempos anteriores declaré un tipo llamado Utf8String
:
type
Utf8String = type AnsiString;
Porque era bastante común tener:
function TUnicodeHelper.WideStringToUtf8(const s: UnicodeString): Utf8String;
begin
Result := WideStringToString(s, CP_UTF8);
end;
y al revés:
function TUnicodeHelper.Utf8ToWideString(const s: Utf8String): UnicodeString;
begin
Result := StringToWideString(s, CP_UTF8);
end;
Ahora en XE6 tengo una función que toma una Utf8String
. Si algún código existente en algún lugar tomara un AnsiString
codificado en UTF-8, e intente convertirlo en UnicodeString usando Utf8ToWideString
, fallaría:
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
O peor, es la amplitud del código existente que hace:
s: Utf8String;
s := UnicodeStringToString(''Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ'', CP_UTF8);
La cadena devuelta quedará totalmente destrozada:
- la función devuelve
AnsiString(1252)
(AnsiString
etiquetada como codificada utilizando la página de códigos actual) - el resultado devuelto se almacena en una
AnsiString(65001)
(Utf8String
) - Delphi convierte la cadena codificada en UTF-8 en UTF-8 como si fuera 1252.
Cómo avanzar
Idealmente, mi función UnicodeStringToString(string, codePage)
(que devuelve un AnsiString
) podría establecer CodePage
dentro de la cadena para que coincida con la página de códigos real usando algo como 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;
Excepto que la manipulación manual con la estructura interna de un AnsiString
es terriblemente peligrosa.
Entonces, ¿qué hay de volver RawByteString
?
Se ha dicho, una y otra vez, por muchas personas que no soy yo que RawByteString
está destinado a ser el receptor universal ; no estaba destinado a ser como un parámetro de retorno:
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;
Esto tiene la virtud de poder usar el SetCodePage
documentado y SetCodePage
.
Pero si vamos a cruzar una línea y comenzar a devolver RawByteString
, seguramente Delphi ya tiene una función que puede convertir un UnicodeString
en una cadena RawByteString
y viceversa:
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;
¿Pero, qué es esto?
O que mas debo hacer?
Este fue un conjunto de antecedentes para una pregunta trivial. La verdadera pregunta es, por supuesto, ¿qué debería hacer en lugar de eso? Existe una gran cantidad de código que depende de UnicodeStringToString
y de lo contrario.
tl; dr:
Puedo convertir un UnicodeString
a UTF haciendo lo siguiente:
Utf8Encode(''Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ'');
y puedo convertir un UnicodeString
a la página de códigos actual usando:
AnsiString(''Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ'');
¿Pero cómo convierto un UnicodeString
a una página de códigos arbitraria (no especificada)?
Mi sensación es que ya que todo realmente es un AnsiString
:
Utf8String = AnsiString(65001);
RawByteString = AnsiString(65535);
Debería morder la bala, abrir la estructura AnsiString
y AnsiString
la página de códigos correcta:
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;
Entonces el resto de la VCL caerá en línea.
Creo que devolver un RawByteString
es probablemente tan bueno como el que obtendrás. Puede hacerlo usando AnsiString
como lo describió, pero RawByteString
captura mejor la intención. En este escenario, un RawByteString
cuenta moralmente como un parámetro en el sentido del consejo oficial de Embarcadero. Es solo una salida en lugar de una entrada. La clave real es no usarlo como una variable.
Podrías codificarlo así:
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;
Entonces
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));
salidas 1252, 1251 y luego 65001 como cabría esperar.
Y puedes usar LocaleCharsFromUnicode
si lo prefieres. Por supuesto, debe tomar su documentación con una pizca de sal: LocaleCharsFromUnicode es un contenedor para la función WideCharToMultiByte . Es increíble que el texto se haya escrito ya que LocaleCharsFromUnicode
seguramente solo existe para ser multiplataforma.
Sin embargo, me pregunto si puede estar cometiendo un error al tratar de mantener el texto codificado ANSI en AnsiString
variables AnsiString
en su programa. Normalmente, se codificaría en ANSI lo más tarde posible (en el límite de interoperabilidad), y también se decodificaría lo antes posible.
Si simplemente tiene que hacer esto, entonces quizás haya una mejor solución que evite AnsiString
completo la temida AnsiString
. En lugar de almacenar el texto en una AnsiString
, guárdelo en TBytes
. Ya tiene estructuras de datos que hacen un seguimiento de la codificación, así que ¿por qué no mantenerlas? Reemplace el registro que contiene la página de códigos y AnsiString
con una página de códigos que contiene y TBytes
. Entonces no tendrías miedo de que nada recodificara tu texto a tus espaldas. Y su código estará listo para usar en los compiladores móviles.
En este caso particular, usar RawByteString
es una solución adecuada:
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 esta manera, RawByteString
contiene la página de códigos, y la asignación de RawByteString
a cualquier otro tipo de cadena, ya sea AnsiString
o UTF8String
o lo que sea, permitirá a RTL convertir automáticamente los datos de RawByteString
de su página de códigos actual a la página de códigos de la cadena de destino (que incluye conversiones a UnicodeString
).
Si absolutamente debe devolver un AnsiString
(que no recomiendo), aún puede usar SetCodePage()
través de un encasillado:
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;
El reverso es mucho más fácil, solo use la página de códigos ya almacenada en una (Ansi|RawByte)String
(solo asegúrese de que esas páginas de códigos siempre sean precisas), ya que RTL ya sabe cómo recuperar y usar la página de códigos por usted:
function StringToWideString(const Source: AnsiString): UnicodeString;
begin
Result := UnicodeString(Source);
end;
function StringToWideString(const Source: RawByteString): UnicodeString;
begin
Result := UnicodeString(Source);
end;
Dicho esto, sugeriría que se eliminen por completo las funciones de ayuda y simplemente se utilicen cadenas escritas. Deje que RTL maneje las conversiones por usted:
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;
Rebasando en System.pas
, encontré la función SetAnsiString
que hace lo que quiero:
procedure SetAnsiString(Dest: _PAnsiStr; Source: PWideChar; Length: Integer; CodePage: Word);
También es importante tener en cuenta que esta función inserta el CodePage en la estructura interna de StrRec para mí:
PStrRec(PByte(Dest) - SizeOf(StrRec)).codePage := CodePage;
Esto me permite escribir algo como:
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;
Así que cuando llamo:
actual := WideStringToString(''Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ'', 850);
me sale el AnsiString
resultante:
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 con la página de códigos apropiada ya rellenada en el miembro secreto de codePage
.
La otra manera
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;
Nota : Cualquier código liberado en el dominio público. No se requiere atribución.