分类目录归档:Develop

在cairo中使用pango文字渲染的文本显示区域是如何计算的

首先,pango可以使用pango_layout_get_pixel_extents获取ink_rect和logical_rect。

// API声明
void pango_layout_get_extents (PangoLayout    *layout,
			  PangoRectangle *ink_rect,
			  PangoRectangle *logical_rect);
// 调用示例
PangoRectangle rcInk, rcLogic;
pango_layout_get_pixel_extents(layout, &rcInk, &rcLogic);

假设使用cairo在(0, 0)位置输出文本”选项1″三个字,使用以上代码获取的 rcInk值为(x=0, y=6, w=52, h=19),rcLogi的值为(0,0,51,30)。那么输出的文字几个关键位置如下:

在绘制时,rcInk.y的值是实际墨迹的y方向起始值,可以理解为文字的顶部空白部分。这个空白在英文字母的绘制中比较明显,比如a会比i的rcInk.y的值更大,因为在四线三格中

垂直居中对齐:上图的是一个文本框的示意图,如果让“墨迹区域”在文本框box内垂直居中显示,可以按如下代码计算oy的值。

rcInk, rcLogic; 
...

topspace = rcInk.y;
oy = (box.h - ink.h) / 2 - topspace;

...
cairo_move(x, oy);
....

底部对齐:底部对齐可按如下方法 计算oy的值

rcInk, rcLogic; 
...

topspace = rcInk.y;
oy = box.h - ink.h - topsace;

...
cairo_move(x, oy);
....

关于windows系统的SetCapture

当我们需要精细化处理某个窗口的WM_MOUSEMOVE事件时,需要使用到SetCapture()。它使我们的程序可以在鼠标移出窗口区域、或在DISABLE的窗口中响应MouseMove事件。

正常情况下,我们会在WM_SETCURSOR事件中设置捕获。然后在鼠标移出窗口区域时释放捕获:

case WM_SETCURSOR:
    SetCapture(hWnd);
    break;

case WM_MOUSEMOVE:
    if (x < 0 || x > width)
        ReleaseCapture();

在Windows系统中,被 SetCapture的窗口在同一时刻只能有一个。并且当一个窗口SetCapture时,其它窗口无论是否处于上层,都将无法响应鼠标事件。所以当两个都需要设置Cature的窗口有重叠区域时,需要额外处理鼠标捕获。

如下图,窗口B在A的上层。当鼠标从起始点进入窗口A时,窗口A会首先SetCapture。但当鼠标向右移动到窗口B时,因为窗口A的Capture,窗口B是无法响应鼠标事件的。如果窗口A已经按上文描述在鼠标移出窗口A时释放捕获。当鼠标继续向右移出窗口A时,窗口B可开始捕获。

在实际应用中,可在MouseMove事件中即时检查鼠标坐标处的顶层窗口。如上图的情形,当鼠标“进入B”时,可在窗口A中释放捕获,交由窗口B继续处理。

字符集与编码方案详细说明

1 文字处理的最朴实场景 – 显示

以文字处理的朴素场景“显示”为例,我们反向推导一下怎么将某个文字在显示器上显示出来。

  • 对显示器来说,它并不知道自己显示的是什么。比如要显示文字“5”,应该给它文字“5”对应的位图数据;
  • 所以,我们首先得从一个地方得到“5”的位图;
  • 可以理解,“5”的位图不是凭空产生的,它必须是预先定义好的一个图片、或一组曲线……其它可显示的文字也应该有各自的绘图数据,这个存储着很多字符的绘图数据的东西,可能在很久以前是一个叫 “汉卡”的硬件、也可能在现在是一个“字体“文件;
  • 如果汉卡或某字体文件中定义了6万个字符,应该怎么找到文字“5”对应的绘图数据呢?
  • 这里出现了一个查询需求。无论是汉卡还是字体文件,它内部必须有一套索引体系,以便调用者查询到某个文字的绘图数据;这套索引实际是一套键值对集合,其中“键”对应要找的字符,“值”对应绘图数据;

到这个地方,整个逻辑就清晰了:

Q:一个信息化系统中,可以显示哪些文字?

A:本系统支持的字符集

Q:在汉卡或字体文件中查询文字的“键”是什么呢?

A:依据本系统中的文字编码方案

Q:各个信息化系统对同一文字,比如大家对“5”的编码方案相同吗?

A:各不相同。但未来的趋势是走向相同

2 字符集

2.1 字符

字符是人类可以理解的最小语言单位,泛指自然语言里所有的文字或符号。在电脑及电信(下文统称信息化)领域,字符是一个信息单位。它大约对应自然语言里的一个单位、符号,或是一个汉字、假名、韩文字,或是一个英文、一个其他西方语言的字母。或是一个音节里的一个音位、类音位……

此外,在信息化领域,还有一个“控制字符”的概念。控制字符并不对应到自然语言中的某个特定符号,而是对应到语言中用来处理文句的概念(类似排版)。如控制打印机的“换行符”、“Tab符”。

所以,我们讲的字符,可分为“可打印字符”和“控制字符”两类。

2.2 字符集

因为字符是人类的主观理解,所以其范围过度宽泛。在电脑及电信领域进行信息化处理时,任何一个子系统需要处理的字符范围都只能是一个有限集。所以标准化组织(国际或国家标准)或国家级行政单位会针对 “需要支持哪些字符”而设定一些规范。这些规定内“字符”的集合就叫字符集。

需要强调的是,字符集规范发布时会为每个字符都指定一个编号,这个编号的本质是一种排序。此编号数值与实际的字符编码数值并不完全一致。虽然大多数信息化系统中ASCII字符的编号正好是编码值。

2.3 编码方案

用什么数值来表示某个字符,是一种规范约定,这种约定即是“编码方案”。一个完整的编码方案,包含两个必须因素:①单个字符的的数据长度;②编码数值与文字的转换方法,这个转换方法可能包含一个编码转换表(代码页)。

2.3.1 早期的代码页方案

由于固定长度数据类型的取值范围是固定的,任何编码方案都是在用有限的整数取值范围去对应不同的字符集。为了支持更多的世界语言, IBM和微软都使用了代码页的概念,在不同语言的系统中使用不同的代码页处理文字。

对于欧洲和西亚语系,可以使用8位整数去对应他们的字符集。一般使用低7位的128个编码空间对应ASCII字符。然后,利用高8位去对应语言内的其它字符。如Windows简体中文系统中使用代码页CP936,繁体中文系统中使用代码页CP950。所以代码页是个字符映射表,早期的Windows系统会带着一个“字符映射表”程序。

各种代码页上的字节流含义是互不相同的。并且,IBM(OEM/IBM PC)的代码页与微软(ANSI)的代码页,即便语言相同,也是互不通用的。一个OEM上的字节流如果想正确的显示在Windows系统中,需要显示指定OME代码页。或显示转换OEM字节流为ANSI字节流,这时需要用到Windows系统的API: OemToChar()。

2.3.2 包含一切的万国码

代码页处理字符的局限显而易见,相同的数据使用不同的代码时,获得了不一样的字符。比如0xB9F1在简体中文CP936里是“国”,在CP950繁体中文里是“弊”。

所以,业界需要一个统一的“字符定义码”去包含世界上所有的字符,这样就可以在同一系统内支持世界语言。

这便是Unicode码,也被称为万国码、单一码、统一码。它是一个包含所有语言、所有字符的排序表,任意一个字符都有一个与之对应的排序号,这个排序号即是字符的Unicode码。Unicode码是一个连续的整数,目前已定义了0x00000-0xfffff区间的用途。①Unicode每隔0xffff区间,也叫1个平面,总共16个平面。

Unicode已被ISO作为国际标准采纳于通用字符集,即 ISO/IEC 10646,Unicode兼容ISO/IEC 10646且完整对应各个版本标准。

3 常见的国家标准字符集

3.1 GB2312

GB/T 2312[注 1],GB/T 2312–80 或 GB/T 2312–1980 是中华人民共和国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,通常简称GB(“国标”汉语拼音首字母),又称GB0,由中国国家标准总局于1980年发布,1981年5月1日实施。GB/T 2312编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB/T 2312。

GB/T 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。

3.2 BIG5

大五码(英语:Big5,又称为五大码)是使用繁体中文(正体中文)社群中最常用的电脑汉字字符集标准,共收录13,060个汉字。

“大五码”(Big5)是由台湾财团法人信息产业策进会为五大中文套装软件所设计的中文共通内码,在1983年12月完成公告,隔年3月,信息产业策进会与台湾13家厂商签定“16位个人电脑套装软件合作开发(BIG-5)项目(五大中文套装软件)”,因为此中文内码是为台湾自行制作开发之“五大中文套装软件”所设计的,所以就称为Big5中文内码。五大中文套装软件虽然并没有如预期的取代国外的套装软件,但随着采用Big5码的国乔中文系统及倚天中文系统先后在台湾市场获得成功,使得Big5码深远地影响繁体中文电脑内码,直至今日。“五大码”的英文名称“Big5”后来被人按英文字序译回中文,以致现在有“五大码”和“大五码”两个中文名称。

3.3 GB13000

GB 13000,中华人民共和国国家标准的国家标准代码之一,全称 GB 13000.1-93《信息技术 通用多八位编码字符集(UCS)第一部分:体系结构与基本多文种平面》。此标准等同采用国际标准化组织 ISO/IEC 10646.1:1993《信息技术 通用多八位编码字符集(UCS)第一部分:体系结构与基本多文种平面》。即“GB 13000.1-93”等同于Unicode 1.1版本。

GB 13000.1-93的字符集包含20,902个汉字,附录是GBK。

2010年1月10日发布的GB 13000-2010取代了GB 13000.1-93,于2010年11月1日实施。是ISO/IEC 10646:2003《信息技术 通用多八位编码字符集(UCS)》等同采用。由中国标准出版社出版,16开1476页。

3.4 GBK

全名为《汉字内码扩展规范(GBK)》1.0版,由中华人民共和国全国信息技术标准化技术委员会1995年12月1日制订,国家技术监督局标准化司和电子工业部科技与质量监督司1995年12月15日联合以《技术标函[1995]229号》文件的形式公布。

GBK的K为“扩展”的汉语拼音(kuòzhǎn)第一个声母。

GBK共收录21886个汉字和图形符号,其中汉字(包括部首和构件)21003个,图形符号883个。

GBK 只为“技术规范指导性文件”,不属于国家标准。国家质量技术监督局于2000年3月17日推出了GB 18030-2000标准,以取代GBK。GB 18030-2000除保留全部GBK编码汉字,在第二字节把能使用范围再度进行扩展,增加了大约一百个汉字及四字节编码空间,但是将GBK作为子集全部保留。

3.5 GB18030

GB 18030,全称《信息技术 中文编码字符集》,是中华人民共和国国家标准所规定的变长多字节字符集。其对GB 2312-1980完全向后兼容,与GBK基本向后兼容,并支持Unicode(GB 13000)的所有码位。GB 18030共收录汉字70,244个。

GB 18030不是一个汉字规范,不定义汉字的写法。中国大陆在此方面的规定由《通用规范汉字表》管理。

3.6 通用规范汉字表

《通用规范汉字表》是《中华人民共和国国家通用语言文字法》的配套规范,是现代记录汉语的通用规范字集,体现着现代通用汉字在字量、字级和字形等方面的规范。2013年6月5日,国务院发出关于公布《通用规范汉字表》的通知,国务院同意教育部、国家语言文字工作委员会组织制定的《通用规范汉字表》,并予公布。《通用规范汉字表》公布后,社会一般应用领域的汉字使用以《通用规范汉字表》为准,原有相关字表停止使用。 该表共收录汉字8105个。

3.7 CJK中日韩统一表意文字

3.7.1 简介

中日韩统一表意文字(英语:CJK Unified Ideographs),也称统一汉字、统汉码(英语:Unihan),目的是要把分别来自中文、日文、韩文、越南文、壮文、琉球文中起源相同、本义相同、形状一样或稍异的表意文字,在ISO 10646及Unicode标准赋予相同编码。

所谓“起源相同、本义相同”、主要是汉字,包括繁体字、简化字、日本汉字(漢字/かんじ)、韩国汉字(漢字/한자)、琉球汉字(漢字/ハンジ)、越南的喃字(𡨸喃/Chữ Nôm)与儒字(𡨸儒/Chữ Nho)、方块壮字(𭨡倱/sawgun)。

3.7.2 历史版本

 Unicode码位版本及数量 
1993.5U+4E00–U+9FFF1.0 20902“〇”(码位U+3007),被当成数字放入了符号和标点区
1999U+3400–U+4DFFExt-A 6582 
2001U+20000–U+2A6FFExt-B 42,711 
2005U+9FA6–U+9FBBExt-B-修订1 22 
2009U+2A700–U+2B734Ext-C 4149 
2010U+2B740–U+2B81FExt-D 224  2012增加1个汉字:U+9FCC
2015U+2B820–U+2CEAF U+9FCD–U+9FD5Ext-E 5776 
2017U+2CEB0–U+2EBEF U+9FD6–U+9FEAExt-F 7488+142018增加5个汉字U+9FEB–U+9FE
  2020U+30000–U+3134A U+9FF0–U+9FFC U+4DB6–U+4DBF U+2A6D7–U+2A6DDExt-G 4939+13+10+6
U+31400–U+33D1F预计放置小篆,U+33E00–U+355FF预计放置甲骨文,相关提案已经提交。按路线图,该平面还会收录金文、简帛文、陶文、鸟虫书等

4 软件开发中的一些问题

4.1 如何读取一个文本文件

读取文本文件的过程是一个将字节流转换为字符串的解码过程。所以必须约定好文本文件中的字符串用什么格式存储。

在Windows系统中,一般使用BOM来指定文本文件的编码方式。即在文本文件的最开头使用一组特定的BOM字节流来指定本文件的文本编码方式。常见的BOM有以下几种。

编码类型BOM(十六进制)BOM (十进制)
UTF-8EF BB BF239 187 191
UTF-16 (BE)FE FF254 255
UTF-16 (LE)FF FE255 254
UTF-32 (BE)00 00 FE FF0 0 254 255
UTF-32 (LE)FF FE 00 00255 254 0 0
UTF-72B 2F 7643 47 118
UTF-1F7 64 4C247 100 76
UTF-EBCDICDD 73 66 73221 115 102 115
SCSU0E FE FF14 254 255
BOCU-1FB EE 28251 238 40
GB-1803084 31 95 33132 49 149 51

在Linux、Apple等系统中,一般不使用BOM,在存储时默认以UTF8存储。读取文本文件时,默认以UTF8方式识别。如果UTF8识别报错,会再尝试以本地语言代码页识别文本。

4.2 源代码中的字符串

在VisualStudio系列软件中,早期的代码文件默认以ANSI方式编码。后来默认以UTF16-LE BOM编码。所以读写源代码文件时,按如下优先级一次识别①识别BOM→②识别为本地代码页。

在跨平台开发中,如果源代码保存为UTF8编码且源文件中包含非ASCII字符时,VisualStudio编译会报无法识别的字符错误。要解决这个问题,可以使用/source-charset:utf-8指定源代码文件的默认编码响应方式。

同样,在GCC系列软件中,默认以UTF8方式编码。早期的GCC可能无法识别BOM头,导致编译报错无法识别的字符。

4.3 内存中的字符串

需要注意的是,VisualStudio开发环境中既区分了源代码的文件编码,又区分了字符串在内存中的编码。对于一个多字节字符串的双引号定义方式,如:const char* text = “大家好”;开发者需要特别关注到此时text中的字符串编码是本地语言的代码页编码。即使文件使用UTF8保存,且指定了/source-charset:utf-8。

也就是说,VisualStudio总是默认以本地语言代码页处理文本。而在GCC环境中,则总是以UTF8方式处理。

std::move操作导致的一个神秘崩溃

原文:The mystery of the crash that seems to be on a std::move operation – The Old New Thing (microsoft.com) January 20th, 2022

客户遇到了只在ARM上出现的程序崩溃。下面是一个简化的版本

-
void polarity_test(std::shared_ptr<Test> test)
{
    test->harness->callAndReport([test2 = std::move(test)]() mutable
    {
        test2->reverse_polarity();
        ::resume_on_main_thread([test3 = std::move(test2)]()
        {
            test3->reverse_polarity();
        });
    });
}
-

他们说,只在第一行就崩溃了:

test->harness->callAndReport([test2 = std::move(test)]() mutable

现在,std::move实际上并不生成任何代码。它只是把引用从左值改为右值,这是一个完全在计算机大脑中(编译阶段)进行的操作。它不需要生成代码。

问题在别的地方。

由于该问题只发生在一个CPU架构(ARM)上,因此后端代码生成器(编译器)可能存在bug。但是为了安全起见,他们联系了编译器前端团队、后端团队(用于代码生成)和库团队(用于shared_ptr)。

我介入并指出,有一个求值顺序(order-of-evaluation)的依赖关系。

test->harness->callAndReport([test2 = std::move(test)]() mutable

语句的左边从test中读取。lambda捕获修改了test(通过std::move将其移动到捕获的变量test2)。

历史上,大多数子表达式的求值顺序是不确定的,虽然有一些操作定义了一个顺序,最明显的是短路表达式(short-circuiting)求值在第二个操作数之前计算第一个操作数(如果有的话)¹。

传统的表达式排序规则不要求在计算参数之前必须决定调用哪个函数。

传统的依赖关系图是这样的²:

test 
 
operator-> 
 
harness 
 
operator->test2 = std::move(test)
callAndReportlambda constructed
function call

由于在左边读取test和在右边修改test之间没有依赖关系,操作可以以任意一种顺序发生。

然后c++ 17出现了。

c++ 17增加了在传统规则之外的额外的求值顺序规则:在下面的表达式中,a在b之前求值:

OperationDescription
a(b)Function call
a[b]Subscript operator
a.*b
a->*b
Pointer to member
a << b
a >> b
Shifting
b = a
b op= a
Assignment
(note: right to left)

就我个人而言,我发现“标准”选择在参数之前对函数求值是很有趣的。在实践中,如果函数是通过指针追踪来识别的,那么首先计算参数会更方便,因为这样做不会干扰很多寄存器。

无论如何,由于函数调用现在在参数之前求值,从c++ 17开始的求值顺序现在要求在lambda中test2 = std::move(test)之前求值左边的test。

因此,问题归结为客户正在使用的语言版本。
客户回来后说,他们正在使用Visual Studio 2019,但使用的是c++ 14模式。
这就解释了。


下次,我们将看看潜在的修复(除了“升级到c++ 17”)。

¹编译器以任何顺序求值的自由情况导致底层体系结构会影响操作顺序。基于堆栈的参数更有可能在基于寄存器参数之前计算:一旦计算了基于寄存器参数的值,就必须在计算其他参数时找到保存它的位置。你可以尝试将它保存在用来传递参数的寄存器中(好),或者你可以尝试将它暂时保存在另一个寄存器中(好),或者你可以spill它并在调用之前重新加载它(坏)。如果其他参数计算起来很复杂,您可能会被迫spill。另一方面,基于堆栈的参数无论如何都会spill到堆栈中,所以您可以计算它并spill它,这样就完成了。在调用之前,您不必复刻(burn)一个寄存器来保存参数。

这意味着即使您只考虑调用约定,最优的求值顺序也可能在x86-32(没有寄存器参数,除了这个)、x86-64/arm(4个寄存器参数)和arm64(8个寄存器参数)之间变化。

²尽管在传统的排序中,我在前面的操作符->之后显示了harness,这并不是语言的规则,是精神内联的产物。真正发生的是

 is

test 
 
operator->&Test::harness
operator->*
(produces harness)

但是 &Test::harness不依赖任何东西。

使用gcov进行linux代码覆盖率测试

1、CMakeLists.txt中更改CMAKE_CXX_FLAGS或在调用cmake时指定 -ftest-coverage -fprofile-arcs

-
cmake -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS} -ftest-coverage -fprofile-arcs" ..
-

2、make 后,代码覆盖率相关的信息会生成文件到“main.cpp.gcno”目录中

3、运行 ./sample,会生成“.gcda”文件

4、使用gcov查看此次运行的代码覆盖率:

-
gcov CMakeFiles/sample.dir/main.cpp.cpp -f
-

5、再看一个100%的示例