作者归档:s

当我尝试解码HttpContent对象返回的字节数据时,为什么会有乱码?

原文:Why is there trailing garbage when I try to decode the bytes of a HttpContent object? – The Old New Thing (microsoft.com)September 23rd, 2021

一个客户无法从HTTP响应中提取正确的文本:

-

winrt::HttpRequest request = ...;

auto result = co_await request.Content().ReadAsStringAsync();

-
-

{"name":"ðAPCDCS±meow"}

-

这个版本生成的字符串看起来基本正常,但有些部分损坏了。

从检查来看,很明显这里有mojibake(🐱),其中一个UTF-8字符串被错误的解读为其它8位字符集。

根据RFC 2616第3.7.1节,如果文本媒体子类型没有显式指定字符集,则默认字符集为ISO-8859-1(Latin-1)。显然,该服务器返回一个编码为UTF-8的字符串,但在报告其Content-Type时未包含显式字符集解释。因此,该字符串默认为ISO-8859-1。

哦~~

现在,RFC 2616的3.4.1节承认,HTTP客户端通常会对缺乏显式字符集解释的返回进行最佳可能性猜测。如果没有提供字符集,Windows在运行时确实会对字符集进行一些猜测:

  • 如果缓冲区以UTF-8 BOM、UTF-16LE BOM、UTF-16BE BOM或GB18030 BOM开头,则根据该字符集对缓冲区进行解码。
  • 如果内容类型是application/json或者是*+json的形式,那么它被解码为UTF-8。

以上实际情况中,服务端既没有显示指定UTF-8 BOM,也没有将内返回容类型设置为application/json。所以,即便他最后返回的是一个json对象,也不满足上面猜测的每一步,而被解释为IOS-8859-1编码。

哦~~

好吧,让我们来尝试在获取服务器响应数据时,将它们显式解码为UTF-8来解决这个问题(明显非常坏的)。

std::wstring Utf8ToUtf16(char const* str)
{
    std::wstring result;
    if (str) {
        auto resultLen = MultiByteToWideChar(
            CP_UTF8, MB_ERR_INVALID_CHARS, str, -1, nullptr, 0);
        if (resultLen) {
            result.resize(resultLen);
            MultiByteToWideChar(
                CP_UTF8, MB_ERR_INVALID_CHARS, str, -1,
                result.data(), resultLen);
        }
    }
    return result;
}

winrt::HttpRequest request = ...;

auto buffer = co_await request.Content().ReadAsBufferAsync();
auto result = Utf8ToUtf16((char const*)buffer.data());

这个版本运行得更好,但却在尾部出现了一些乱码:

-

{"name":"🐱meow"}SOH

-

在这种情况下,问题不是缓冲区数据,而是如何将缓冲区转换为字符串。data()方法返回一个指向缓冲区开头的指针,代码将其作为源字符串传递给Multi-Byte-To-Wide-Char,字符串长度为-1。

这个特殊值-1意味着该指针应被视为以空结束的字符串的开始位置。但是Read-As-Buffer-Async生成的Buffer只是从服务器返回的原始字节,服务器不会在末尾放一个空结束符。服务器说,“响应是19个字节长”,它发送19个字节,就这样。

因此,这个尾部的乱码是读缓冲区溢出,代码只是读过缓冲区的真实末端后,并“一直继续解码,直到达到空结束符”为止。

当您希望解码缓冲区中的字节数据时,您需要指定缓冲区中的长度,而不是说“一直继续解码,直到达到空结束符”。

std::wstring Utf8ToUtf16(char const* str, int32_t inputLen)
{
    std::wstring result;
    if (str) {
        auto resultLen = MultiByteToWideChar(
            CP_UTF8, MB_ERR_INVALID_CHARS, str, inputLen, nullptr, 0);
        if (resultLen) {
            result.resize(resultLen);
            MultiByteToWideChar(
                CP_UTF8, MB_ERR_INVALID_CHARS, str, inputLen,
                result.data(), resultLen);
        }
    }
    return result;
}

winrt::HttpRequest request = ...;

auto buffer = co_await request.Content().ReadAsBufferAsync();
auto result = Utf8ToUtf16(
    (char const*)buffer.data(),
    static_cast<int32_t>(buffer.Length()));

以上代码将生成实际所需的字符串解码为UTF-8,没有乱码。

现在,您不需要编写代码来获取包含UTF-8编码字符串的Buffer并将其转换为UTF-16字符串。Windows运行时已经提供了一个帮助函数:

-
winrt::HttpRequest request = ...;

auto buffer = co_await request.Content().ReadAsBufferAsync();
auto result = CryptographicBuffer::ConvertBinaryToString(
    BinaryStringEncoding::Utf8, buffer);
-

但由于我们使用的是c++ /WinRT,我们完全可以使用 the conversion built into C++/WinRT we learned last time去避免这些问题。其中难点部分是从缓冲区中获取std::string_view 。

winrt::HttpRequest request = ...;

auto buffer = co_await request.Content().ReadAsBufferAsync();
auto result = winrt::to_hstring(
    std::string_view{
        static_cast<char const*>(buffer.data()),
        buffer.Length() });

好了,你可以这样,读取原始缓冲区并将其从UTF-8转换为UTF-16字符串。

与此同时,去修复你的服务器。

尾声:客户最终发现他们的服务器确实有错误。响应是通过回调服务器生成的,它将Content-Type报头放在ResponseHeaders中而不是contenttheaders。

使用pango在windows中渲染文字的一些细节(2)

上篇文章使用pango在windows中渲染文字的一些细节(1)大致描述了pangov1.45.3中存在的的稍嫌不足之处,本文尝试去解决这些问题。

一、为pango提供SurrogateFallback(替换性备选字体)支持

1、第一步,实现windows系统读取SurrogateFallback的代码

read_windows_surrogate_fallback是在pango的read_windows_fallbacks()中使用,实现时直接传入line_buffer变量,并将当前字体的“替换性备选字体”追加进line_buffer变量中。

static void
read_windows_surrogate_fallback(GString* line_buffer, const wchar_t* fontname)
{
    HKEY hKey;
    LSTATUS status;

    wchar_t szPath[261];
    swprintf_s(szPath, 261, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\LanguagePack\\SurrogateFallback\\%s", fontname);
    status = RegOpenKeyExW(HKEY_LOCAL_MACHINE, szPath, 0, KEY_READ, &hKey);
    if (status != ERROR_SUCCESS)
    {
        swprintf_s(szPath, 261, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\LanguagePack\\SurrogateFallback");
        status = RegOpenKeyExW(HKEY_LOCAL_MACHINE, szPath, 0, KEY_READ, &hKey);
    }
    if (status != ERROR_SUCCESS)
        return;

    // Unicode当前规范包含从0x00000-0xfffff总共16个平面(panel)
    for (int i = 2; i < 16; i++)
    {
        wchar_t szPanel[16];
        DWORD dwSize;

        swprintf_s(szPanel, 16, L"Plane%d", i);

        dwSize = sizeof(szPath) / sizeof(szPath[0]);
        status = RegGetValueW(hKey, NULL, szPanel, RRF_RT_REG_SZ, NULL, szPath, &dwSize);
        if (status == ERROR_SUCCESS)
        {
            gchar* sutf8 = g_utf16_to_utf8(szPath, -1, NULL, NULL, NULL);
            g_string_append_printf(line_buffer, ",%s", sutf8);
            g_free(sutf8);
        }
    }

    RegCloseKey(hKey);
}

2、第二步,在pango的read_windows_fallbacks函数中增加SurrogateFallback支持

static void
read_windows_fallbacks (GHashTable *ht_aliases)
{
  ...
  for (value_index = 0; status != ERROR_NO_MORE_ITEMS; value_index++)
    {
      ...
      status = RegEnumValueW (hkey, value_index, name, &name_length,
                              NULL, NULL, NULL, NULL);
      ...
      utf8_name = g_utf16_to_utf8 (name, -1, NULL, NULL, NULL);
      ...
      g_string_append_printf (line_buffer,
                              "\"%s\" = \"%s",
                              utf8_name,
                              utf8_name);
      ...
      // 优先加入SurrogateFallback(替换性备选字体)
      read_windows_surrogate_fallback(line_buffer, name);

3、显示效果对比

下图是在windows11(10.0.22000.184)中选用MingLiU字体的显示效果对比。

第一行的pango渲染没有使用SurrogateFallback(替换性备选字体)支持,可以看见数字、英文、及汉字𩽾的显示均与MingLiU字体整体风格不符,尤其是汉字𩽾。

使用pango在windows中渲染文字的一些细节(1)

书写此文的背景

2021年9月使用cairo实现DirectUI时,发现有些特殊文字无法正常显示。

分析cairo及pango的代码后,发现pango虽然已通过fontmap方式解决了大部分文字渲染问题,但在处理Unicode非0平面(Plane)的字符时,仍存在不合理之处。

文字渲染的问题

1、严重错误:某些字符被渲染成“其它字符”比如将”𩽾29F7E“渲染成“齾9f7e“。这是cairo的一个BUG,不具普遍性。

2、无法显示:某些字符被渲染成一个“方块”或“数字码”,这是普遍的文字渲染问题,存在于很多软件中。Windows系统已经处理的很好,而且有完善的编程接口。可以参考:关于Windows TextOut输出文字时的一些难看的“方块”

3、显示不好:已设置为字体A,但部分字符被渲染为其它字体。比如经常看到某些软件或网页中的“少部分”文字的显示效果与其它文字不一样。这也是一个普通性问题。

cairo及pango的现状

一、cairo(v1.16)获取文字glyph索引的方式是完全错误的

没有考虑4字节(2个wchar_t)文字的情况,代码片段如下:

    wchar_t unicode[2];
    ...
    unicode[0] = ucs4;
    unicode[1] = 0;
    if (GetGlyphIndicesW (hdc, unicode, 1, &glyph_index, 0) == GDI_ERROR) {
	...
	glyph_index = 0;
    }
  • cairo官方不建议使用其自身的cairo_show_text渲染文字,而是使用pango。

二、pango(v1.45.3)已通过fontmap解决了“无法显示”的问题

指定字体“无法显示”某个字符的根本原因是:单个字体文件的可包含字符数(2^16)远小于Unicode字符总数(16*2&^16)。当待渲染字符没有被当前字体包含时,将产生“无法显示”问题。

所以在pango中使用fontmap将多个字体关联在一起, 只要这些关联字体的任意一个包含待渲染文字的glyph定义,即可完成渲染。

例如:SimSun(宋体)可以关联MINGLIU(细明)、MSYH(微软雅黑)等字体,渲染字符时,如果宋体里找不到该字符,就再找细明体、微软雅黑体。使用此方式可以解决绝大部分的“无法显示”问题

pango的默认代码中包含了两种fontmap:

1、依据Windows的fontlink信息,fontlink的数据存储在如下注册表位置

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink

2、写在代码内的几个默认定义。开发者可以扩展此方式创建自己的fontmap,还算灵活

static const char * const builtin_aliases[] = {
  "courier = \"courier new\"",
  /* It sucks to use the same GulimChe, MS Gothic, Sylfaen, Kartika,
   * Latha, Mangal and Raavi fonts for all three of sans, serif and
   * mono, but it isn't like there would be much choice. For most
   * non-Latin scripts that Windows includes any font at all for, it
   * has ony one. One solution is to install the free DejaVu fonts
   * that are popular on Linux. They are listed here first.
   */
  "sans = \"dejavu sans,tahoma,arial unicode ms,lucida sans unicode,browallia new,mingliu,simhei,gulimche,ms gothic,sylfaen,kartika,latha,mangal,raavi\"",
  "sans-serif = \"dejavu sans,tahoma,arial unicode ms,lucida sans unicode,browallia new,mingliu,simhei,gulimche,ms gothic,sylfaen,kartika,latha,mangal,raavi\"",
  "serif = \"dejavu serif,georgia,angsana new,mingliu,simsun,gulimche,ms gothic,sylfaen,kartika,latha,mangal,raavi\"",
 "mono = \"dejavu sans mono,courier new,lucida console,courier monothai,mingliu,simsun,SimSun-ExtB,gulimche,ms gothic,sylfaen,kartika,latha,mangal,raavi\"",
  "monospace = \"dejavu sans mono,courier new,lucida console,courier monothai,mingliu,simsun,gulimche,ms gothic,sylfaen,kartika,latha,mangal,raavi\"",
  "emoji = \"segoe ui emoji,segoe ui symbol,segoe ui\"",
  "cursive = \"commic sans ms\"",
  "fantasy = \"gabriola,impact\"",
  "system-ui = \"yu gothic ui,segoe ui,meiryo\"",
};

三、pango(v1.45.3)未考虑Windows的SurrogateFallback机制

Windows的SurrogateFallback机制可以指定Unicode每个平面(Plane)的字符渲染特定字体。这样的直接定位方式,可以避免过大的fontmap产生的效率问题。

例如,Win32系统的如下定义里,第一个”Plane2″=”SimSun-ExtB”指定所有无定义的字体在渲染Plane2字符时,均使用SimSun-ExtB渲染。第二个”Plane2″=”SimSun-ExtB”,特指SimSun字体在渲染Plane2字符时使用SimSun-ExtB渲染。

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\LanguagePack\SurrogateFallback]
"Plane2"="SimSun-ExtB"

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\LanguagePack\SurrogateFallback\SimSun]
"Plane2"="SimSun-ExtB"

四、pango(v1.45.3)未考虑Windows的Font Substitution

Windows的Font Substitution支持在渲染时将字体A替换为字体B,并且可以精确到具体字符集。也就是说,它支持将字体A中的字符集0替换为字体B中的字符集121进行渲染。这些设置保存在如下注册表中:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontSubstitutes

窗口行为与绘制行为的区分

1、窗口中的空间在鼠标移动时需要更改状态

2、窗口行为: 鼠标事件、窗口刷新、输出到屏幕

3、绘制行为: 控件状态、具体绘制

win32:

wndproc: 开发者实现, 默认的处理函数

rander: vui2lib

user:

1)mousemove/lbutton/rbutton 到 wndproc

2) 转换为event到vui2lib, vui2lib更改控件状态

3)wndproc 判断改变, 并刷新

4)event的后续处理, 如: 事件回调

如何在对话框中设置控件的焦点

在对话框中设置焦点时,只调用SetFocus是不够的.

在MSDN介绍DM_SETDEFID的文档中指出, 对默认ID的不当操作可能导致奇怪的情况, 例如在对护框中包含连个默认按钮. 幸运的是, 你很少改变对护框的默认按钮.

一个更大的问题是使用SetFocus函数来在对护框中设置焦点. 如果这么做, 那就是直接进入到窗口管理器, 而绕过了对话框管理器. 这就意味着将可能出现一些“不合理”的情况, 例如吧焦点放到一个按钮上, 而这个按钮不是默认按钮.

为了避免这个问题, 不要使用SetFocus来改变对话框上的焦点,而是使用WM_NEXTDLGCTL消息.

void SetDialogFocus(HWND HDlg, HWND hwndControl)
{
  SendMessage(Hdlg, WM_NEXTDLGCTRL, (WPARAM)hwndControl,  TRUE);
}

为什么SetFocus不会管理默认的ID

windows对话框管理器是建立在窗口管理器之上的. 由于SetFocus是窗口管理器中的函数. 因此, 它不知道接收焦点的窗口是否是在某个对话框内.如果只是检查父窗口的窗口类是不是对话框, 还是不够的. 同样, 在前面已经看到了, 在应用程序中可以使用IsDialogMessage函数在窗口中支持键盘导航(使用键盘控制窗口), 而这些窗口看你根本就不是对话框. 窗口管理器不能只是发送DM_GETDEFID消息到拥有焦点窗口的父窗口, 因为DM_GETDEFID在数值上等于WM_USER, 而WM_USER在窗口类内部定义的消息范围之内. 如果父窗口不是对话框, 那么当我们向窗口发送WM_USER消息时, 结果是不可预知的.