Подтвердить что ты не робот

Преобразование 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 будет падать в линию.

4b9b3361

Ответ 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;

Примечание. Любой код, выпущенный в общественное достояние. Не требуется атрибуция.