【问题标题】:Reading UTF8 (24-bit) and outputting读取 UTF8(24 位)并输出
【发布时间】:2022-01-12 23:44:16
【问题描述】:

我有一个 UTF8 文本文件,其中包含 8 位和 24 位 UTF8 编码(ASCII 和日语)。

我的目标是逐个读取字符,然后输出如下内容:

{'V', 0x02},

这是一个C++语言初始化入口,其中第一个参数是UTF8编码,第二个参数是文件内的索引或位置。

我的第一步是成功读取UTF8文本文件并输出到控制台。

这是输入文件的示例:

 ()+-0123456789Vanoty˄˅いおがきくさしすせただてでなにのびまみむめりるれをん  

这是我的程序的输出示例:

$ ./main.exe
Size of wchar_t: 2
32
40
41
43
45
48
49
50
51
52
53
54
55
56
57
86
97
110
111
116
121
4294953604
4294953605
4293034116
4293034122
4293034124
4293034125
4293034127
4293034133
4293034135
4293034137
4293034139
4293034143
4293034144

这是我的代码:

#include <fstream>
#include <iostream>

int main()
{
    std::cout << "Size of wchar_t: " << sizeof(wchar_t) << "\n";
    std::ifstream japanese_file("japanese_font_glyphs_all_112_qty_horizontal_layout.txt", std::ios::binary);

    char c = '\0';
    char32_t    utf8_char = 0;
    while (japanese_file.read(&c, 1))
    {
        unsigned int bytes_in_encoding = 1u;
        if ((c & 0x80u) == 0U)
        {
            bytes_in_encoding = 1u;
        }
        else
        {
            if ((c & 0xF0u) == 0xE0u)
            {
                bytes_in_encoding = 3u;
            }
            else
            {
                if ((c & 0xE0u) == 0xC0u)
                {
                    bytes_in_encoding = 2u;
                }
            }
        }
        char32_t    utf8_encoding = 0u;
        switch (bytes_in_encoding)
        {
            case 1:
                utf8_char = c;
                break;
            case 2:
            {
                char c2 = 0;
                japanese_file.read(&c2, 1);
                utf8_char = (c * 0x100ul) + c2;
            }
                break;
            case 3:
            {
                char c2 = 0;
                japanese_file.read(&c2, 1);
                char c3 = 0;
                japanese_file.read(&c3, 1);
                utf8_char = (c * 0x10000ul) + (c2 * 0x100ul) + c3;
            }
                break;
            default:
                break;
        }
        std::cout << utf8_char << "\n";
    }
    
    japanese_file.close();

    return EXIT_SUCCESS;
}

输出显示wchar_t的大小为2,不足以容纳日文字形的24位编码。

那么,我应该使用什么代码将 24 位 UTF8 编码(作为单个字形)输出到控制台?

设置
g++ (GCC) 10.2.0 -- Cygwin
视窗 10
视觉工作室 2017

我正在编写的应用程序将在 Windows 10 上作为控制台应用程序运行。

编辑 1 -- 背景
我的应用程序将生成 C++ 数据语句,用于为显示芯片的位图寄存器创建索引。

这是结构定义和一些示例条目:

struct UTF8_To_Bitmap_Index_t
{
    char32_t    encoded_character;      //!< UTF8 encoding.
    uint8_t     bitmap_index;           //!< Index of glyph within the font.
    uint8_t     padding_alignment;      //!< For alignment purposes, not used.
};
static const
UTF8_To_Bitmap_Index_t default_conversion_table[] =
{
    {'¡', 0x01, 0u}, 
    {'À', 0x02, 0u}, 
    {'Á', 0x03, 0u}, 
    {'Ã', 0x04, 0u}, 
    {'Ä', 0x05, 0u}, 
    {'Å', 0x06, 0u}, 
};

【问题讨论】:

  • 呃,两个字节(Windows 上的 wchar_t)对于几乎所有晦涩难懂的日语字符来说已经绰绰有余了。您的 UTF-8 解析错误。为什么不使用图书馆? Windows 已安装 ICU,您可以使用 ICU 库轻松处理所有这些。
  • 正确的名称是“UTF-8”。而且每个字符最多可以遇到 4 个字节,而不仅仅是 24 位。
  • @AmigoJack 每个 encoded 字符的字节数无关紧要(我总是接受 CESU-8,但如果你想严格一点,那么 UTF-8 最多为 4八位字节)。每个解码“字符”的字节数各不相同,但如果您的意思是每个代码点,那么它是三个 == 24位。但是,对于 BMP 之外的代码点,您只需要第三个八位字节。 (所有普通的日语代码点都在 BMPinside 中。)因此,如果 OP 坚持使用 BMP,那就没关系了。但如果是这样,由于 Windows 使用 UTF-16,更喜欢使用库进行解码和编码。 ICU 内置在所有 Win OS 中,并且可以很好地处理这一点 - 使用它。
  • @Dúthomhas:BMP 有大约 60000 个字符。两个八位字节的 UTF8 编码具有110xxxxx 1yyyyyyy 的形式,因此根据鸽巢原理,我们可以证明绝大多数 BMP 字符都需要 3 个八位字节的 UTF-8。平假名和片假名很可能是两个八位字节,但仅汉字就太多了,无法放入两个 UTF-8 八位字节。
  • 哦,看来我误解了 OP 想要解码 UTF-8 数据的愿望。我不确定他打算如何使用编码值进行字体字形查找,但是,嘿,输出的第一个示例显示了一个 Unicode 代码点,最后一个也是如此。也许OP可以澄清......?

标签: c++ utf-8 visual-studio-2017 windows-10 g++


【解决方案1】:

编辑 → 以下假设您希望将 UTF-8解码为 Unicode 码位值,这对于查找表和查找字体中的字形很有用。

(免责声明:我没有安装 cygwin;以下使用 MinGW-w64 并假定命令行兼容 bash。您可能需要针对 cygwin 的怪异进行调整。)

要查找和使用 Windows 的 ICU,您需要执行几个步骤。打开一个 shell 提示符(bash/zsh/cygwin 给你的任何东西)并输入:

cd /mnt/c/Program\ Files\ \(x86\)/Windows\ Kits/

→ 请记住,您可以随时点击TAB 来帮助您。

找到 ICU 标头。

find ~+ -name 'icu*.h'

您将获得至少一个目录中的文件列表。选择版本号最大的目录。对我来说,它是“10.0.17763.0”。这是您将添加到 CPATH 的路径(确保使用正确的字符大写并转义这些空格和括号):

(Makefile) CPATH += /mnt/c/Program\ Files\ \(x86\)/Windows\ Kits/10/Include/10.0.17763.0/um
(Terminal) export CPATH=/mnt/c/Program\ Files\ \(x86\)/Windows\ Kits/10/Include/10.0.17763.0/um/

请参阅下面的示例,了解如何处理源代码中实际包含的标头。

查找 ICU 库文件

find ~+ -name 'icu*.lib'

选择与上面相同的版本号并选择正确的架构。对于大多数现代 PC,它是“x64”。这就是 GCC 令人讨厌的地方:你不能只将它添加到你的 LIBRARY_PATH 中。相反,您必须在命令行中指定程序源代码的完整路径。

/mnt/c/Program\ Files\ \(x86\)/Windows\ Kits/10/Lib/10.0.17763.0/um/x64/icuin.Lib
/mnt/c/Program\ Files\ \(x86\)/Windows\ Kits/10/Lib/10.0.17763.0/um/x64/icuuc.lib

作弊,您可以将它们复制到工作目录(我将假设您在下面的示例中这样做;-)

编译

g++ -Wall -Wextra -pedantic-errors -O3 -std=c++17 -o example example.cpp icuin.Lib icuuc.lib
strip example.exe

MSVC对应的命令行是:

cl /EHsc /W4 /Ox /std:c++17 example.cpp

示例程序

请注意,在旧的 Windows 控制台 中使用纯 C 或 C++ 工具将 UTF-8 打印到控制台有点棘手且不一致。新的 Windows 终端 让一切按应有的方式运行。下面的代码假定新的 Windows 终端 - 如果没有帮助,它将无法在旧的 Windows 控制台上正确显示。

(如果您必须在较旧的 Windows 控制台上正确显示,我发现 wprintf() 往往最常工作,但我不打扰,因为让它在编译器之间运行是不可能的。我只是在一个修改后的 rdbuf 附加到输出流 IFF,它们附加到控制台。这样 C++ 代码可以使用std::cout 以通常的方式编写。)

“example.txt”文件与您在问题中提供的文本相同,但任何 UTF-8 编码文件都可以。

第 47 行的 BUFFER_SIZE 值故意很小,以突出 ICU 的 ucnv_toUnicode 功能的工作原理(完全按照您的要求执行)。我个人会使用更大的缓冲区,从 100 到 1024 个元素,具体取决于您希望占用多少堆栈空间。

另一种方法是使用std::vector 作为输入和输出缓冲区——您甚至可以提供一个默认参数缓冲区大小来选择它。请注意您的要求,不要过度设计。

如上所述,包含哪些 ICU 文件取决于奇怪的 Windows 版本控制疯狂。如果您的系统上没有可用的“icu.h”,那么您必须使用旧的“icucommon.h”和“icui18n.h”文件。通过在命令行中添加 -DSUPPRESS_LEGACY_ICU_HEADER_WARNINGS 来实现。

以下代码使用函数构造std::map。注意文件偏移量必须是键值,否则会发生冲突。当然,您可以只打印结果而忘记返回地图。这只是如何使用 ICU 解决问题的一个示例。

#include <ciso646>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <string>

using namespace std::string_literals;


#ifdef SUPPRESS_LEGACY_ICU_HEADER_WARNINGS
  #include <icucommon.h>
  #include <icui18n.h>
#else
  #include <icu.h>
#endif

#ifdef _MSC_VER
  #pragma comment(lib, "icuuc")
  #pragma comment(lib, "icuin")
#endif


// Function to convert bytes from file --> map of {file offset, Unicode code point}

std::map <size_t, char32_t>
map_utf8_file_offsets( const std::filesystem::path & filename )
{
  std::map <size_t, char32_t> result;

  // This is how we get {offset,uchar} pairs -- using a UTF-8 UConverter

  UErrorCode err;
  std::shared_ptr <UConverter> ucnv8
  (
    ucnv_open( "UTF-8", &(err=U_ZERO_ERROR) ),
    []( UConverter * ucnv8 ) { if(ucnv8) ucnv_close( ucnv8 ); }
  );
  if (!ucnv8.get()) throw u_errorName( err ) + " (Could not create UTF-8 converter)"s;

  // We'll process the file in small chunks

  std::ifstream f( filename, std::ios::binary );
  if (!f) throw "Failure to open file: " + filename.string();

  constexpr int BUFFER_SIZE = 10;  // adjust to your liking
  char          octets    [BUFFER_SIZE];
  UChar         codepoints[BUFFER_SIZE];
  int32_t       offsets   [BUFFER_SIZE];
  UChar       * p_codepoint;
  const char  * p_octet;
  size_t        offset_base = 0;

  // For each chunk
  while (f.read( octets, BUFFER_SIZE ) or f.gcount())
  {
    // Convert it
    ucnv_toUnicode
    (
      ucnv8.get(),
      &(p_codepoint=codepoints), codepoints+BUFFER_SIZE,
      &(p_octet    =octets),     octets    +f.gcount(),  // true: gcount ≤ BUFFER_SIZE
      offsets,
      f.gcount()!=BUFFER_SIZE,
      &(err=U_ZERO_ERROR)
    );
    if (err) throw u_errorName( err ) + " (UTF-8 conversion error)"s;

    // Store results
    auto n_codepoints = p_codepoint - codepoints;
    for (int n = 0;  n < n_codepoints;  n++)
    {
      result[ offset_base+offsets[n] ] = codepoints[n];
    }
    offset_base += BUFFER_SIZE;
  }
  ucnv_resetToUnicode( ucnv8.get() );

  return result;
}


// Helper to print Unicode characters to the console since cout/wcout can't do it directly

std::string to_utf8( char32_t c )
{
  UErrorCode err = U_ZERO_ERROR;
  char s[5];
  int32_t n = 0;
  UChar uc = (UChar)c;
  return u_strToUTF8( s, 5, &n, &uc, 1, &err );
}


int main()
try
{
  for (const auto & [offset, c] : map_utf8_file_offsets( "example.txt" ))
  {
    std::cout << "{'" << to_utf8( c ) << "', " << offset << "},\n";
  }
}
catch (const std::string& s)
{
  std::cerr << s << "\n";
  return 1;
}

嗯,就是这样。

【讨论】:

  • 计划是写入控制台,以便可以将数据复制并粘贴到另一个项目的源文件中。我有一个包含 127 个项目的“转换”表,我真的不想手动完成(因为可能存在拼写错误)。
  • 我可以轻松切换到 Visual Studio 2017,如果这样可以更轻松地进行开发。
  • 编译器的选择无关紧要。对于您想要的后端实用程序,您甚至不需要编译器——Python 或 Tcl 解释器会容易得多。我在上面制作的示例代码将是 Tcl 脚本中的十行左右。那么,您在上面提供的结构是绝对的:char32_t encoded_character 是 UTF-8 编码的字节字符串(并且 不是 解码的 Unicode 代码点值)?如果是 UTF-8 字符串,是存储大端还是小端,还是匹配机器的字节序? (我假设是 x64 处理器,它是小端序的?)继续...
  • ...继续:输出整数值而不是字符文字是否更有意义?您是否不想在输出中列出第三个成员值,即使它只是 , 0 },
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-17
  • 1970-01-01
  • 2023-03-26
  • 2015-01-19
  • 1970-01-01
相关资源
最近更新 更多