Преобразование UnicodeString в AnsiString
В старые времена у меня была функция, которая преобразует WideString
в AnsiString
указанной кодовой страницы:
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;
И все сработало. Я передал функцию строку юникода (т.е. Кодированные данные UTF-16) и преобразовал ее в AnsiString
с пониманием того, что байты в AnsiString
представлены символами с указанной кодовой страницы.
Например:
TUnicodeHelper.WideStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 1252);
возвращает Windows-1252
закодированную строку:
The qùíçk brown fôx jumped ovêr the lázÿ dog
Примечание. Информация, конечно же, была потеряна во время преобразования из полного набора символов Юникода в ограниченные пределы кодовой страницы Windows-1252:
-
Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ
(до) -
The qùíçk brown fôx jumped ovêr the lázÿ dog
(после)
Но Windows WideChartoMultiByte
выполняет довольно хорошую работу по наилучшему сопоставлению; как это предусмотрено.
Теперь после времени
Теперь мы находимся в разы. WideString
теперь является парией, а UnicodeString
- добротой. Это несущественное изменение; поскольку функция Windows требовала только указателя на серию WideChar
(что также есть UnicodeString
). Поэтому вместо объявления UnicodeString
мы используем декларацию:
funtion WideStringToString(const Source: UnicodeString; CodePage: UINT): AnsiString;
begin
...
end;
Теперь мы возвращаемся к возвращаемому значению. У меня есть AnsiString
, который содержит байты:
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
В прежние времена это было хорошо. Я отслеживал, какая кодовая страница AnsiString
действительно содержалась; я должен был помнить, что возвращенный AnsiString
не был закодирован с использованием локали компьютера (например, Windows 1258), но вместо этого был закодирован с использованием другой кодовой страницы (кодовая страница CodePage
).
Но в Delphi XE6 an AnsiString
также тайно содержит кодовую страницу:
- codePage: 1258
- длина: 44
- значение:
The qùíçk brown fôx jumped ovêr the lázÿ dog
Эта кодовая страница неверна. Delphi указывает кодовую страницу моего компьютера, а не кодовую страницу, в которой находится строка. Технически это не проблема, я всегда понимал, что AnsiString
был на определенной кодовой странице, я просто должен был обязательно передать эту информацию.
Поэтому, когда я хотел декодировать строку, мне пришлось пройти по кодовой странице с ней:
s := TUnicodeHeper.StringToWideString(s, 1252);
с
function StringToWideString(s: AnsiString; CodePage: UINT): UnicodeString;
begin
...
MultiByteToWideChar(...);
...
end;
Затем один человек закручивает все вверх
Проблема заключалась в том, что в прежние времена я объявлял тип с именем Utf8String
:
type
Utf8String = type AnsiString;
Потому что достаточно распространено:
function TUnicodeHelper.WideStringToUtf8(const s: UnicodeString): Utf8String;
begin
Result := WideStringToString(s, CP_UTF8);
end;
и наоборот:
function TUnicodeHelper.Utf8ToWideString(const s: Utf8String): UnicodeString;
begin
Result := StringToWideString(s, CP_UTF8);
end;
Теперь в XE6 у меня есть функция, которая принимает a Utf8String
. Если какой-то существующий код где-то взял кодировку UTF-8 AnsiString
и попытался преобразовать ее в UnicodeString с помощью Utf8ToWideString
, это завершится неудачно:
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
Или, что еще хуже, это ширина существующего кода:
s: Utf8String;
s := UnicodeStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', CP_UTF8);
Возвращенная строка будет полностью искажена:
- функция возвращает
AnsiString(1252)
(AnsiString
, помеченную как закодированная с использованием текущей кодовой страницы)
- результат возврата сохраняется в строке
AnsiString(65001)
(Utf8String
)
- Delphi преобразует кодированную строку UTF-8 в UTF-8, как если бы она была 1252.
Как двигаться вперед
В идеале моя функция UnicodeStringToString(string, codePage)
(которая возвращает AnsiString
) может установить CodePage
внутри строки, чтобы она соответствовала фактической кодовой странице, используя что-то вроде 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;
За исключением того, что ручная работа с внутренней структурой AnsiString
опасна.
Так как насчет возврата RawByteString
?
Было сказано, что многие люди, которые не я, который RawByteString
должен быть универсальным получателем; он не должен быть как возвращаемый параметр:
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;
Это означает, что вы можете использовать поддерживаемый и документированный SetCodePage
.
Но если мы собираемся пересечь строку и начать возвращать RawByteString
, конечно, у Delphi уже есть функция, которая может преобразовать строку UnicodeString
в строку RawByteString
и наоборот:
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;
Но что это такое?
Или что еще я должен делать?
Это был длинный набор фона для тривиального вопроса. Реальный вопрос, конечно, что я должен делать вместо этого? Существует много кода, который зависит от UnicodeStringToString
и обратного.
ТЛ; др:
Я могу преобразовать a UnicodeString
в UTF, выполнив:
Utf8Encode('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ');
и я могу преобразовать UnicodeString
на текущую страницу кода, используя:
AnsiString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ');
Но как мне преобразовать UnicodeString
в произвольную (неуказанную) кодовую страницу?
Я чувствую, что, поскольку все действительно является AnsiString
:
Utf8String = AnsiString(65001);
RawByteString = AnsiString(65535);
Я должен укусить пулю, распаковать структуру AnsiString
и вставить в нее правильную кодовую страницу:
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;
Затем остальная часть VCL будет падать в линию.
Ответы
Ответ 1
В этом конкретном случае использование RawByteString
является подходящим решением:
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;
Таким образом, RawByteString
содержит кодовую страницу и присваивает RawByteString
любому другому типу строки, будь то AnsiString
или UTF8String
или что-то еще, позволит RTL автоматически преобразовывать RawByteString
данные из текущей кодовой страницы в кодовую страницу целевой строки (которая включает преобразования в UnicodeString
).
Если вы абсолютно должны вернуть AnsiString
(который я не рекомендую), вы все равно можете использовать SetCodePage()
с помощью typecast:
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;
Обратное гораздо проще, просто используйте кодовую страницу, уже сохраненную в (Ansi|RawByte)String
(просто убедитесь, что эти кодовые страницы всегда точны), поскольку RTL уже знает, как получить и использовать кодовую страницу для вас:
function StringToWideString(const Source: AnsiString): UnicodeString;
begin
Result := UnicodeString(Source);
end;
function StringToWideString(const Source: RawByteString): UnicodeString;
begin
Result := UnicodeString(Source);
end;
Говоря это, я бы предложил отказаться от вспомогательных функций и просто использовать набранные строки. Пусть RTL обрабатывает конверсии для вас:
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;
Ответ 2
Я думаю, что возвращение RawByteString
, вероятно, так же хорошо, как и вы. Вы можете сделать это, используя AnsiString
, как вы начертили, но RawByteString
лучше отражает намерение. В этом случае а RawByteString
морально считается параметром в смысле официального совета Embarcadero. Это всего лишь выход, а не вход. Настоящий ключ не должен использовать его как переменную.
Вы можете его прописать следующим образом:
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;
Тогда
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));
выводит 1252, 1251, а затем 65001, как и следовало ожидать.
И вы можете использовать LocaleCharsFromUnicode
, если хотите. Разумеется, вам нужно его документацию с щепоткой соли: LocaleCharsFromUnicode - это оболочка для функции WideCharToMultiByte. Удивительно, что текст был когда-либо написан, так как LocaleCharsFromUnicode
наверняка существует только для кросс-платформенной.
Однако, интересно, можете ли вы ошибиться при попытке сохранить ANSI-кодированный текст в переменных AnsiString
в вашей программе. Обычно вы должны кодироваться в ANSI как можно раньше (на границе взаимодействия) и также декодировать как можно раньше.
Если вам просто нужно это сделать, возможно, есть лучшее решение, которое полностью устранит страшный AnsiString
. Вместо сохранения текста в AnsiString
сохраните его в TBytes
. У вас уже есть структуры данных, которые отслеживают кодировку, поэтому почему бы не сохранить их. Замените запись, содержащую кодовую страницу и AnsiString
, на одной, содержащей кодовую страницу, и TBytes
. Тогда вы бы не боялись ничего переписывать текст за спиной. И ваш код будет готов к использованию в мобильных компиляторах.
Ответ 3
Пробираясь через System.pas
, я нашел встроенную функцию SetAnsiString
, которая делает то, что я хочу:
procedure SetAnsiString(Dest: _PAnsiStr; Source: PWideChar; Length: Integer; CodePage: Word);
Также важно отметить, что эта функция действительно меняет CodePage во внутреннюю структуру StrRec для меня:
PStrRec(PByte(Dest) - SizeOf(StrRec)).codePage := CodePage;
Это позволяет мне написать что-то вроде:
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;
Итак, когда я звоню:
actual := WideStringToString('Ŧĥε qùíçķ ƀřǭŵņ fôx ǰűmpεď ōvêŗ ţħě łáƶÿ ďơǥ', 850);
Я получаю результирующий AnsiString
:
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'
An AnsiString с соответствующей кодовой страницей, уже заполненной секретным членом codePage
.
Другой способ
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;
Примечание. Любой код, выпущенный в общественное достояние. Не требуется атрибуция.