详解UspLib库中如何使用Uniscribe


Uniscribe 导读

ScriptString API

ScriptString API适用于那些用单一字体和单一颜色信息来显示文本的程序,如记事本(标准windows编辑器控件) 是应用scriptstring API的一个主要例子。此API中一个非常好的特性是它允许你显示文本的一串字符,作为”被选定”部分而被显示出来。这实际上是非常好的体验,它节省了你很多的工作量。

 

ScriptStringAnalyze函数是Uniscribe的起点。这个函数看起来相当复杂吓人。不过它的目的还是被用于完成Unicode 文本字符串的整形(shaping)和字形替换(glyph-generation),当它工作完成时,它的返回值是一个SCRIPT_STRING_ANALYSIS 结构。

 

HRESULT WINAPI ScriptStringAnalyse (

  HDC                     hdc,

  void                    * pString,

  int                       cString,

  int                       cGlyphs,

  int                       iCharset,

  DWORD                     dwFlags,

  int                       iReqWidth,

  SCRIPT_CONTROL          * psControl,

  SCRIPT_STATE            * psState,

  int                     * piDx,

  SCRIPT_TABDEF           * pTabdef,

  BYTE                    * pbInClass,

  SCRIPT_STRING_ANALYSIS  * pssa

);

















SCRIPT_STRING_ANALYSIS是一个不透明结构,没有文档介绍它的内容细节。这并不重要,因为这种结构体只是作为参数直接传给其他scriptstringAPI使用,而不需要任何其它知识来使用它。

 


HRESULT WINAPI ScriptStringOut (

  SCRIPT_STRING_ANALYSIS    ssa,

  int                       iX,

  int                       iY,

  UINT                      uOptions,

  RECT                    * prc,

  int                       iMinSel,

  int                       iMaxSel,

  BOOL                      fDisabled

);

ScriptStringOut用于显示先前分析的文本字符串。请注意,在这个函数调用中,文本字符串不是被直接指定的,仅是通过SCRIPT_STRING_ANALYSIS结构传进来的所包含的必要信息来显示原始字符串。

 

HRESULT WINAPI ScriptStringXtoCP (

  SCRIPT_STRING_ANALYSIS    ssa,

  int                       iX,

  int                     * piCh,

  int                     * piTrailing

);

ScriptStringXtoCP是个有趣的函数。它提供了一种机制,用于Unicode文本字符串中的插入符和鼠标的定位。

 

HRESULT WINAPI ScriptStringCPtoX (

  SCRIPT_STRING_ANALYSIS    ssa,

  int                       icp,

  BOOL                      fTrailing,

  int                     * pX

);

ScriptStringCPtoX对应ScriptXtoCP。它执行相反的任务 - 转换字符串位置到显示坐标。

 

HRESULT WINAPI ScriptStringFree(

  SCRIPT_STRING_ANALYSIS  * pssa 

);

当程序完成显示字符串后,ScriptStringFree函数被用于最后的清理工作。我们还有许多ScriptString函数没有在这里列出,但只用以上这五个函数就够用了,可以用最小的代价实现一个全功能的文本编辑器。



 

上面的图片显示了我写的一个用于演示ScriptStringAPI的小程序,。源代码和demo文件可以在这篇文章的顶部下载。

ScriptString的一个奇怪之处:如果用于render的设备上下文和分析该字符串时ScriptAnalyze的设备上下文不一样,将导致ScriptStringOut失败!

 

 

UspLib简介

ScriptString API 的主要问题是它不能用一个以上的字体和颜色信息来显示一段文本。这使它特别不合适我们开发的 Neatpad。我们唯一的选择是直接使用低阶的 Uniscribe函数。

 

USPLib是我写的一个提供比ScriptString具有更丰富功能的库。这个新的库包装了周围低阶的Uniscribe API,我们将在接下来的几个教程深入讨论它们。 UspLib在方法上非常类似于ScriptString,但多了很多进一步的文本着色控制和格式设置。

 

USPDATA * USP_Allocate();

第一个函数是​​USP_Allocate。这个函数返回一个指向USPDATA对象的指针,此对象将被用于后续UspLib的操作。

 

BOOL USP_Analyze (

  USPDATA   * uspData,

  HDC         hdc,

  WCHAR     * wstr,

  int         wlen,

  ATTR      * attrRunList,

  UINT        flags,

  USPFONT   * uspFont

);

USP_Analyze类似于ScriptStringAnalyze 。所不同的是,一个文本字符串,可以使用现有的USPDATA对象重新分析。

 

void USP_ApplyAttributes (

  USPDATA  * uspData,

  ATTR     * attrRunList

);

一旦一个字符串被分析(即初始化(itemized)和整形(shaped) 等等),可以在任何时候使用USP_ApplyAttributes 重新应用颜色属性(colour-attributes)。存储在attrRunList中的字体信息(font-information)将被忽略。

 

void USP_ApplySelection (

  USPDATA  * uspData,

  int        selStart,

  int        selEnd

);

USP_ApplySelection执行类似于USP_ApplyAttributes的任务。然而这一次只是修改USPDATA对象中的selection-flags。

 

int USP_TextOut (

  USPDATA  *  uspData,

  HDC         hdc,

  int         xpos,

  int         ypos,

  RECT     *  rect);

USP_TextOut对应ScriptStringOut。它输入一个以前分析的USPDATA对象作为参数,并将其绘制到指定位置。任何被应用到文本的字体信息,颜色信息和高亮选择都将被绘制出。

 

void USP_Free(USPDATA * uspData);

USPDATA 对象如果长时间不再需要,应调用USP_Free进行清理。我将在接下来的两三个教程详述如何使用UspLib,并将直接提供使用Uniscribe的详细细节和例子。


 

Uniscribe的奥秘

上一个教程非常简要的介绍了UniscribeScriptString API。不幸的是,ScriptStringAPI因为只能使用单一的字体和颜色信息的限制而不能被Neatpad使用。 因此,本教程的目的是研究 “低阶”Uniscribe API。这是因为我们有非常具体的要求,我们需要一个具有multi-font,syntax-coloured的Neatpad文本编辑器。

 

如下所示的Unicode文本的字符串将被用作我们讨论的基础。这个字符串中间使用阿拉伯语词汇, 不是因为他们有什么特别的意义,仅仅因为它们的Unicode属性适合我们的讨论背景。


你已经发现,相对字符串的其他部分,在阿拉伯短语中有两个“字形”已经以不同的颜色高亮显示。这两个字形的字母是“U0633阿拉伯字母SEEN ”和“U0627阿拉伯字母ALEF ” 。如果隔离它们后显示如下:



左边框中显示的是上下文整形后(假设你使用的是支持Unicode的网络浏览器如InternetExplorer,并安装适当的字体)渲染的两个字符。这是我们要求字形达到的行为效果。在右边框中显示的是各自分开呈现的字符。如果两个框中内容看起来是一样的,那么你的浏览器显示Unicode的方式是不正确得。

 

Unscribe存在一大原因就是提供了一种如上所述的复杂的"整形"行为。要办到这一点,这要求我们不拆分Unicode 字符串为单个字符。这种人为拆分会打破字符之间的整形行为。因此本教程的主要目的是解释如何在不拆分字符的情况下能以不同颜色绘制单个字符而同时仍保持字符的上下文整形效果。

 

基本纲要

绘制文本的基本步骤概括如下。注意,我们省略了自动换行(和ScriptBreak API)的步骤。假设我们有一段utf-16 Unicode文本字符串, 我们要做的是:

1.      ScriptItemize——将字符串分割成各种scripts或“item-runs”。

2.      结合item runs 和程序定义的”style” runs,以产生更细粒度的items。

3.      ScriptLayout - 有可能重排items。

 

然后对于每个item/runs(由ScriptLayout结果所决定的顺序)

 

4.      ScriptShape——应用上下文整形行为,从每个run中转换字符为一系列字形。

5.      ScriptPlace——计算run中每个字形的宽度和位置。

6.      应用着色/高亮到每个独立的字形。

7.      ScriptTextOut - 显示字形。

 

这一纲要紧随Microsoft建议使用UniscribeAPI的步骤。但请注意,我们有MSDN没提到的额外步骤#6(text-colouring)。 我们将通过本教程的深入学习,来揭示这种差别背后的原因。我将把自动换行的主题留到以后的教程中分析,这是因为自动换行的问题更多的是行-缓冲管理的问题,而不是使用Uniscribe的问题。

 

BOOL UspAnalyze (

  USPDATA         * uspData,  

  HDC               hdc,

  WCHAR           * wstr,

  int               wlen,

  ATTR            * attrRunList,

  UINT              flags,

  SCRIPT_TABDEF   * tabDef,

  USPFONT         * uspFont                          

);

上面的函数原型是一个名为UspAnalyze的函数。这是我为Neatpad写的新UspLib文本渲染引擎的一部分。 ScriptStringAnalyze 和UspAnalyze在很多方面是相似的,但是UspAnalyze允许调用者指定字符串的字体和样式信息此类附加功能。

 

本教程的其余部分将开始侧重研究如前所述各个阶段的Uniscribe API的方方面面, 并且每个阶段都将讨论任何可能出现的问题。在每一阶段,我们会看看实现UspAnalyze函数的关键点。

 

1.   ScriptItemize

当显示一个Unicode文本字符串时, ScriptItemize通常是第一个被调用的Uniscribe函数。

它的目的是要识别出一个字符串中的各种Scripts。然和根据这些Script,把字符串分割成

Items(或者runs)。


 

 

上表说明了由ScriptItemize 处理utf-16 字符串“HelloيُساوِيWorld”的结果。字符显示的是逻辑顺序 - 换句话说,这是它们存储在内存中的顺序。该字符串被分为三段。请注意items纯粹出自它们的Script - 而不是通过出现在字符串中更细粒度的字形和字形集群产生。

 

 

HRESULT WINAPI ScriptItemize(

  WCHAR          * wszText,       // pointer to unicode string

  int              wszLength,     // count of WCHARs        

  int              cMaxItems,     // length of pItems buffer

  SCRIPT_CONTROL * psControl,   

  SCRIPT_STATE   * psState,

  SCRIPT_ITEM    * pItems,        // out - array of SCRIPT_ITEM structures

  int            * pcItems        // out - count of items

);

 

ScriptItemize 的返回值是一组SCRIPT_ITEM 结构数据,这组数据的每笔内容是,从文本分段产生的每个“可成形”的item(script)。 被返回结构体的个数就被存储在 *pcItems中。在上面的例子中,*pcItems值为“3”。 SCRIPT_ITEM结构非常简单,如下所示。

 

 

struct SCRIPT_ITEM

{

   int              iCharPos;

   SCRIPT_ANALYSIS  a;

};

 

SCRIPT_ITEM::iCharPos变量是用来确定每个“run”在文本中某个字符串的起始位置。成员a的SCRIPT_ANALYSIS结构拥有很多的额外信息,包括run的读取方向和整形引擎,它被使用于转换run到一系列字形。

 

下图说明了Unicode字符串如何分割为一组SCRIPT_ITEM结构:


注意始终有个"隐藏"SCRIPT_ITEM作为这组数据的结尾。这使得可以构造以下计算式来计算每个SCRIPT_ITEM 的长度:

 

itemLength = pItems[i+1].iCharPos - pItems[i].iCharPos;

 

在这里有几个值得注意的点。请注意ScriptItemize 的第一个参数是WCHAR *。所以从现在起Neatpad 将是个纯粹的Unicode 应用程序,没有ANSI 版本。除非我们可以使用Microsoft的 Unicode(MSLU)层,否则我们会要放弃对 Win9x 的支持。

 

还要注意的是你永远无法提前知道一个文本字符串中将有多少个SCRIPT_ITEM数据被返回,因此通常需要使用某种形式的循环- 直到调用ScriptItemize的SCRIPT_ITEM缓冲区获得足够的内存而成功返回:

 

SCRIPT_CONTROL scriptControl = { 0 };

SCRIPT_STATE   scriptState   = { 0 };

 

SCRIPT_ITEM   *itemList      = 0;

int            itemCount;

 

do {

    itemList = realloc(itemList, ... );

 

    hr = ScriptItemize(

            wstr,

            wlen,

            allocLen,

            &scriptControl,

            &scriptState,

            itemList,

            &itemCount);

 

    if(hr != S_OK && hr != E_OUTOFMEMORY)

        break;

 

} while(hr != S_OK);

 

警告——确保你总是通过完全初始化SCRIPT_CONTROL和SCRIPT_STATE结构到ScriptItemize中,即使其内容被完全初始化为 “零”,也要这么做。除非这些结构都是指定的,不然Unicode双向算法将不会被用于正在分项(itemizing)的字符串上。这样在某些情况下,就可能导致不正确识别item-run位置 (如LTR和RTL 脚本(script )出现在同一个字符串中的时候)。

 

有趣的是,MSDN说,当SCRIPT_CONTROL和SCRIPT_STATE为NULL时的分项(itemization)是纯粹以字符代码为基础的。当非NULL时,如上面所说的应用全双向算法。对于后一种情况,整段必须在内存中。虽然我不打算走这条路,但这确实表明了一种处理整个段落的任意长度的文本行不能驻留在内存中的方法。

 

2.   Merging Style Runs

我们非常直接地使用 Uniscribe而不使用 ScriptString 函数的原因是因为我们想要对文本着色和字体选择有比较细粒度的控制。在我们现在已达到的点(调用 ScriptItemize后),  Microsoft 文档建议我们要合并"应用程序定义"的style runs和通过ScriptItemize返回的item信息。这是来自 MSDN 陈述:

 

“在使用Uniscribe之前,应用程序将段落划分为runs,即划分出具有相同风格的一串字符。 这些风格取决于应用程序的实现,但通常包括的一些属性,比如字体、大小和颜色...., 合并item信息与run信息,以生产单一的风格,脚本(script)和方向的runs。”

 

这句话是整个 Uniscribe 文档中最令人困惑、神秘和误导性的语句之一。因为真正的问题是,在 MSDN 中,没有任何说明该如何合并style-runs 到item-runs,又或者有任何提示"style run"实际上是什么。因此我们将进一步了解如何"合并 runs",但首先我们要弄明白"style run"一词的含义。

 

当然,一个style-run是任何程序期望的。它在本质上是文本的一个范围,这个范围中分配一组特定属性集给其中的字符串。在Neatpad的情况下,我已经使用了ATTR结构来表示颜色和字体 - 在一个文本字符串中为每个字符提供此属性。文本字符串和属性列表(attribute-list)看起来像这样:

WCHAR buff[ MAXLINELEN ];

ATTR  attr[ MAXLINELEN ];

 

然而,由于迁移到Uniscribe和‘inversion-highlighting’方案,我已经稍微扩展了ATTR的结构,所以它不再是一个‘每个字符对应一个ATTR’的概念 :

struct ATTR

{

   COLORREF     fg;    // 前景文本色

   COLORREF     bg;    //背景文本色

   int  len   : 16;    // length of this run (in WCHARs)

   int  font  : 7;     // font-index

   int  sel   : 1;     // selection flag (yes/no)

   int  ctrl  : 1;     // show as an isolated control-character

};

 

前景色和背景色保持不变。新的结构成员详述如下。

 

l  第一个变化是一个新的长度字段,用于表示attribute-run的长度(以字符为单位)——仅仅是因为Uniscribe喜欢处理“runs”相关的事情,而不是处理单个字符。我还不至于修改现有Neatpad代码(假设1 ATTR对应1个字符), 在处理Uniscribe API时,这个额外的字段将被用于“内部管理”。

l  字体字段和以前比也没有什么不同。它仍然是作为字体表(font-table)的一个索引。

l  该sel布尔值用于指示text-run的选择状态 - 换句话说,这个run将将以选择-高亮状态渲染。这是一个很重要的变化 - 我不再存储selection-colours到 :: FG和::BG中– 而用一个独立的标志来表示一个字符(或文字范围)是否被选中。这样的变化是为了适应“inversion-highlighting”的方案。

l  ctrl布尔值是最后一个添加项,用于指示是否应该以正常状态渲染text-run字符(字符串),还是作为独立的控制字符(控制字符串)。

 

我们现在面临的问题是,我们有两个毫无关联的实体-SCRIPT_ITEM列表(SCRIPT_ITEMlist),它标识了script在原始字符数组中的位置信息和 ATTR列表(ATTR list),它标识了样式在原始字符串中的范围信息。我们需要弄明白MSDN所说的合并这两个不相关列表的意思:

 

SCRIPT_ITEM *itemList;

ATTR        *attrList;

 

基本处理方法是同时看看style-runs和item-runs, 确认在字符串中的任何位置上是否有一种类型run重叠另一种类型run的地方。例如,假设一个SCRIPT_ITEM run和ATTR 结构之间的边界位置相重叠。此SCRIPT_ITEM 将会被拆分成两个新的部分 - 每个部分包含了不同的ATTRstyle-run。

 

拆分的方法是这样的: SCRIPT_ITEM::iCharOffset变量被修改为指向原始字符串中新的位置,以及建立ITEM_RUN结构数组来保存这些新的字符位置。SCRIPT_ITEM的其余内容(即指SCRIPT_ANALYSIS结构)必须被复制成两份,使它们之间各自都持有一份。这个想法来自于:ScriptItemize函数首先打断字符串为基于script为单位的离散单元。然后合并处理步骤进一步打断字符串为基于style为单位的更小的单元。在两者之间应该会有一些重叠。

 

希望下面的关系图能够阐明何谓“样式合并(style merge)”:


 

现在这里有个问题。如果我们打断了SCRIPT_ITEM,会不会影响到Uniscribe 引擎的上下文整形行为的效果呢? 简短的回答是:是的,我们拆分字符串的行为,这将会打乱Uniscribe的整形行为 – 没有任何神奇的方法可以绕过这一问题。

 

你可能会注意到,紧随ATTR styleruns下面,我写了个“fontonly”。这个是有目的的,因为微软建议我们以基于style为单位来打断字符串,这并不真的正确。事实上,在这个阶段根据颜色的不同(以选择/语法高亮为目的而设置的不同颜色)来打断字符串是错误的做法:

 

当合并style-runs 和item-runs时,我们必须只考虑字体信息(fonts),而完全

忽略掉颜色信息(colour-information)。

 

希望我已经准备的足够充分来解释这点。继微软文档的建议后,我浪费了大约一个星期的时间来试图找出如何着色一个字符串,后来才意识到,我走错了方向。语法着色(或者任何与此有关的文本着色种类)必须在整形已经发生之后,才能被应用到字符串– 即在ScriptShape和 ScriptPlace被调用之后,ScriptTextOut调用之前。这并不是说我们不能在我们的ATTR结构体中存储颜色– 只是我们不能再使用这些信息同时进行’合并’处理。在做任何“分割”之前,任何共享相同字体的ATTR结构,必须通过merge处理过程,合并到单一的run中。

 

好了,那么我们一旦拆分了ATTR结构和SCRIPT_ITEM结构,然后我们该做些什么呢?我已经定义了一个名为ITEM_RUN的新结构, 它包含了来自ATTR结构和SCRIPT_ITEM结构 中所需要的内容:

 

struct ITEM_RUN

{

   SCRIPT_ANALYSIS  analysis;      // from the original SCRIPT_ITEM

   int              charPos;       // character-offset within the original string

   int              len;           // length of run in WCHARs

   int              font;          // only font is required, not colours

   ...

};

 

ITEM_RUN基本上可以让我们保持“格式化”的信息与item-runs并置。一旦我们已经分项(itemized)了字符串。Uniscribe就只关心每个run的SCRIPT_ANALYSIS结构。ITEM_RUN结构的其他成员供我们个人使用。item-run-list被存储在USPDATA结构体中。下面是itemRunList字段:

 

struct USPDATA

{

   ITEM_RUN   * itemRunList;

   int          itemRunCount;

   ...

};

 

合并runs的算法实际上是相当复杂的 - 事实上,它是Uniscribe编程棘手的一个方面,不得不承认这样一个事实:微软放弃了任何帮助和提示,来说明如何处理这个棘手的问题,除了给CSSamp应用程序1998年写的一篇文章“Windows NT 5.0中关于支持多语言文本布局和复杂的脚本”。

 

为了解决这个问题,我已经写了一个名为BuildMergedItemRunList的新函数,它为给定的Uniscribe字符串构建ITEM_RUN结构数组。它会执行两个任务 - 调用ScriptItemize ,然后根据指定的attrList 合并style-runs。

 

BOOL BuildMergedItemRunList(

                 USPDATA  * uspData,       // in/out - holds results of merge

                 WCHAR    * wstr,         

                 int        wlen,

                 ATTR     * attrList,     

 );

 

BuildMergedItemRunList是USPLib的一个私有函数,在构建USPDATA对象时,BuildMergedItemRunList作为 UspAnalyze的第一个步骤被调用。 现在分离出来,函数的使用是这样的:

 

 

ATTR attrList[2] =

{

    { RGB(0xff, 0x00, 0xff), RGB(0,0,0), 5, 0, 0 },    // five characters using font#0

    { RGB(0xAA, 0x22, 0xAA), RGB(0,0,0), 6, 1, 0 }     // six  characters using font#1

}

 

BuildMergedItemRunList(uspData, L"Hello World", 11, attrList);

 

明白使用Uniscribe的很大优势是来自对上下文整形和复杂脚本的支持。通过再次分割SCRIPT_ITEM结构将一个Unicode字符串切片细分,将破坏我们所寻求的脚本整形(script-shaping)行为。我们必须尽量保持最小化分割SCRIPT_ITEM – 但是有基于不同颜色进行分割这个阶段的存在是不正确的。虽然Neatpad将在显示文本之前用Uniscribe建立ATTR style-list,但也要在整形已经发生之后才能使用ATTR style-list的颜色信息。

 

最后,如果你正在开发一个文本编辑器,并且打算它仅需要处理单个字体,那么你完全可以跳过这个阶段,节省自己大量的工作(或者甚至只是使用ScriptString API,如果你不想使其具有语法着色的能力!)

 

3.   ScriptLayout

在Uniscribe中的下一个阶段是获取合并的item-runs,建立正确的视觉显示顺序。在我们的例子中,我们使用由BuildMergedItemRunList生成的 ITEM_RUN数组结构。这是一个重要的步骤,是正确显示双向文本的关键。注意,除非一个字符串包含从右到左的脚本(script),否则重排不是必需要做的,但我们仍然需要这么做,这是因为我们直到运行时才会知道我们会处理什么样的脚本和语言。

 

Uniscribe的ScriptLayout函数被调用来执行重排任务,并使用Unicode的双向算法来实现这个任务。

 

HRESULT WINAPI ScriptLayout(

   int     cRuns,

   BYTE  * pbLevel,              // in

   int   * piVisualToLogical,    // out

   int   * piLogicalToVisual     // out

);

 

ScriptLayout将输入一个简单的字节数组,用来代表字符串的双向 run-嵌入级别 - 每个item-run对应一个字节。每个ITEM_RUN的run-嵌入级别值存储在SCRIPT_STATE::uBidiLevel变量中。我们需要在调用ScriptLayout之前建立这个BYTE[] 数组。


 

 

我们必须从每个item-run中手动解压出uBidiLevel变量。作为SCRIPT_STATE结构的成员,uBidiLevel被深埋于每个SCRIPT_ANALYSIS内。一旦BYTE []数组被建立,ScriptLayout API就可以调用它了。这一切看来似乎是相当大量的工作却只是为了进一步返回一个整数数组,事情就是这样的。想必Uniscribe的开发商这么做,是因为他们假定您将创建和合并自己的ITEM_RUN (或类似)结构。

 

VOID BuildVisualMapping( ITEM_RUN *  itemRunList,

                         int         itemRunCount,

                         int         visualToLogicalList[]  // out

  )

{

    int     i;

    BYTE  * bidiLevel = malloc(itemRunCount * sizeof(BYTE));

 

    // Manually extract bidi-embedding-levels ready for ScriptLayout

    for(i = 0; i < itemRunCount; i++)

        bidiLevel[i] = itemRunList[i].analysis.s.uBidiLevel;

 

    // Build a visual-to-logical mapping order

    ScriptLayout(itemRunCount, bidiLevel, visualToLogicalList, NULL);

 

    // free the temporary BYTE[] buffer

    free(bidiLevel);

}

 

上面的函数演示了如何从给出的ITEM_RUN数组结构中获取visual-mapping列表。这个列表是必不可少的,不管是显示文本字符串时,或做任何需要视觉-顺序的处理,如鼠标/插入符命中测试,都需要这个列表。

 

int xpos = 0, ypos = 0;

 

for(visualIdx = 0; visualIdx < itemRunCount; visualIdx++)

{

    int logicalIdx    = visualToLogicalList[visualIdx];

    ITEM_RUN *itemRun = itemRunList[logicalIdx];

 

    ProcessRun(itemRun, xpos, ypos);

 

    xpos += itemRun->width;

}

 

以上这种循环处理的方式是必需的,因为我们可能涉及到使用从右到左的脚本(scripts) (即阿拉伯语或希伯来语),当这样来显示文本时,我们仍然一切按从左到右的方式绘制,包括‘backwards’runs也这样处理。visual-to-logical列表提供从视觉映射到逻辑索引的方法,并确保我们总是以适当的顺序处理runs。


 

更多Uniscribe的奥秘

Uniscribe 的奥秘续...

我们将直接从我们的最后一个教程处开始,仔细回顾这些Uniscribe API。记住,我们仍然工作在UspAnalyze函数中,上次的事件序列点是我们已经打断了一串Unicode文本字符到item-runs中。下面是我们走到这一步的步骤:

 

1.ScriptItemize - 将字符串分割成各种scripts或“item-runs”。

2.合并item runs 和程序定义的”style” runs,以产生更细粒度的items。

3.ScriptLayout - 有可能重排items。

 

这项工作的结果是生成ITEM_RUN结构数组(称为itemRunList )和visual-logical mapping数组(称为visualToLogicalList ) - 它告诉我们以什么样的顺序来显示runs。这两个数组存储在USPDATA对象内部:

 

struct USPDATA

{

    ...

 

    ITEM_RUN  * itemRunList;

    int         itemRunCount;

    int       * visualToLogicalList;

    ...

};

 

接下来的任务是依次使每个item-run呈现一些文本(使用ScriptTextOut)。 这将为每个run调用Uniscribe密切相关的两个函数(ScriptShape和ScriptPlace)。 下面是我们现在将遵循的步骤:

 

4.      ScriptShape——应用上下文整形行为,从每个run中转换字符为一系列字形。

5.      ScriptPlace——计算run中每个字形的宽度和位置。

6.      应用着色/高亮到每个独立的字形。

7.      ScriptTextOut - 显示字形。

 

4. ScriptShape

在所有的Uniscribe函数中,ScriptShape可能是最重要的。它的目的是转换Unicode字符的runs为一系列准备显示的字形。ScriptShape取代由GetCharacterPlacement API提供的功能,但是它们返回的数据的类型非常类似。

HRESULT WINAPI ScriptShape(

   HDC                hdc,

   SCRIPT_CACHE     * psc,

   const WCHAR      * pwsChars,         // in

   int                cChars,

   int                cMaxGlyphs,

   SCRIPT_ANALYSIS  * analysis,         // in

   WORD             * pwOutGlyphs,      // out - array of glyphs

   WORD             * pwLogClust,       // out - glyph cluster positions

   SCRIPT_VISATTR   * psva,             // out - visual attributes

   int              * pcGlyphs          // out - count of glyphs

);

 

调用此函数返回的信息组结果让人眼花缭乱。让我们看看每个参数,依次了解他们所代表的含义。

 

l  psc是SCRIPT_CACHE对象指针。 这个对象在首次调用ScriptShape之前,必须初始化为空。

l  pwsChars和cChars 共同标识构成当前runs的 Unicode文本的范围(来自原始字串的一串)。

l  analysis是每个run指向SCRIPT_ANALYSIS结构的指针。

l  pwOutGlyphs[]是一块WORD值缓冲区,它接受构成run的"glyph-indices"数据。glyph-index是一个挂钩特定字体的值 - 它是标识在字体中特定字形图像的值。该pwOutGlyphs缓冲区的大小必须由cMaxGlyphs参数来指定。当ScriptShape返回时,将在* pcGlyphs中存储pwOutGlyphs项目的数量。

l  psva[]是一块SCRIPT_VISATTR结构缓冲区。这个数组和字形列表( pwOutGlyphs )同步,所以必须分配相同大小的尺寸。除了作为一个需要被输入到ScriptPlace 中,我至今还没有发现SCRIPT_VISATTR信息的任何其它使用。

l  pwLogClust []为WORD值数组。在文本的run中,每个字符(character,16位WCHAR )对应一个WORD,所以pwLogClust的每个元素完全对应到原始文本的字符(character)位置。这也意味着, pwLogClust缓冲区的大小必须和文本的run长度相同- cChars被作为长度单位。

 

这里最重要的参数是pwLogClust []数组,其中的内容可以用来在逻辑字符位置和字形簇(glyph-cluster)位置之间进行映射。我们将在下一个教程中研究这个数组更多的细节。

 

字体后备

大多数字体不支持完整的Unicode字符定义的范围。 事实上我还不知道有哪个字体可以显示所有的Unicode脚本和语言。 最近的一个例子是“Arial Unicode MS” - 它在Microsoft Office cd中 - 但即使这个字体有大约55000个字符,也不能完全显示所有的字形。 缺少的字体字形通常(但不总是) 显示为小方块。

 

应用程序通常通过利用针对每个Unicode脚本类型的特定字体来解决这个问题。这种处理方法被称为字体后备,它实现了当主显示字体(例如,一个文本编辑器)没有包含相应的字形来渲染字符串的所有文字(characters)时, 从内部查找表中查找一个“备份字体”,用符合喜好要求的字形替代主字体中缺失的字形。

 

字体后备不被低阶的Uniscribe API所支持 - 只有ScriptString API有这个设施。因此所有以Uniscribe为基础的程序,都需要一个内置的后备字体列表。因为这个原因,我不打算在UspLib中实现字体后备功能。支持字体后备,以及分析文本的每一行时,要替换字体能够被指定到ATTRstyle-runs中,都将是Neatpad要负责的事情。

 

5. ScriptPlace

ScriptPlace要使用ScriptShape的输出结果(glyph-index-list和 SCRIPT_VISATTR 列表),以产生字形的前向宽度(advance-width)信息。前向宽度(Advance-widths)是从一个字形到下一个字形简单的像素偏移值。这个信息被返回在整型数组(piAdvance)中,能够被用于定位显示文本时的输出坐标,也可以用于鼠标的命中测试。

 

HRESULT WINAPI ScriptPlace(

   HDC                hdc,

   SCRIPT_CACHE     * psc,

   WORD             * pwGlyphs,       // in - the results from ScriptShape

   int                cGlyphs,        // in - number of glyphs in pwGlyphs

   SCRIPT_VISATTR   * psva,           // in - from ScriptShape

   SCRIPT_ANALYSIS  * analysis,       // in - from the ITEM_RUN

   int              * piAdvance,      // out - array of advance widths

   GOFFSET          * pGoffset,       // out - array of GOFFSETs

   ABC              * pABC            // out - pointer to a single ABC structure

);

 

而不是接受一个WCHAR 字符缓冲区作为输入(如ScriptShape那样做),ScriptPlace需要ScriptShape产生的glyph-indices缓存区作为输入。值得注意的参数是:

l  pwGlyphs[](和相应的cGlyphs)和ScriptShape返回的字形数组相同。

l  psva[]是ScriptShape返回的SCRIPT_VISATTR数组。

l  piAdvance[]整型缓冲区指针,它会为run接受前向宽度值列表。每个pwGlyphs中的字形对应一个piAdvance中的一项。所以piAdvance数组因此必须被分配出和pwGlyphs相同尺寸项。

l  pGoffset[]是一个GOFFSET结构缓冲区指针。这个结构体标识每个字形应该被显示位置的偏移值。MSDN文档混淆了这个参数作为一个单独的GOFFSET结构– 但是pGoffset也必须被分配和pwGlyphs数组同样的长度。

 

最后,item-run的宽度是由ABC结构指针pABC来表示的。每个run的总宽度能够使用下面的式子计算出来:

runWidth = abc.abcA + abc.abcB + abc.abcC;

 

请注意,相同的值也可以通过计算piAdvance数组中所有的整数值求和计算出。

 

for(i = 0; i < uspData->itemRunCount; i++)

    ShapeAndPlaceItemRun(hdc, &uspData->itemRunList[i]);

 

ScriptPlace非常依赖ScriptShape的结果,以至于这两个函数通常都划分到一个包装函数中,被同时调用。ShapeAndPlaceItemRun函数就是用于这种效果,字符串的每个item-run都会调用一次这个函数。

 

制表符延展

在Uniscribe中处理tabs真的很容易,即使是没有内建支持。要理解的事情是,任何原始文本字符串中的字符在调用ScriptShape后,总是将被至少一个字形所表示。这甚至适用于那些不可显示的控制符,如回车符,空格,当然还有制表符(tab characters)。

 

为了说明这个观点,字符串”Hello”将被作为例子,在其中已经嵌入了两个TAB字符:


 

 

下表保存的是在这个文本字符串上调用ScriptShape和ScriptPlace后的结果:

 

Array

[0]

[1]

[2]

[3]

[4]

[5]

[6]

pwGlyphs[]

43

3

72

79

3

79

82

piAdvance[]

165

0

102

64

0

64

115

 

注意,这两个制表符的glyph-index都被表示为”3”。然而这个glyph-index是仅对于一个特定字体有效的,它表示一个‘不可显示’的字形 – 也就是说,一个字形没有视觉表示。更有趣的是这个‘无形’字形的宽度值,被初始设置为零”0”。

 

正常的过程中我们到了调用ScriptTextOut的阶段,也生成了上面所示的宽度+字形。这将会有下面的结果:


 

虚线外形纯粹是用在这里跨越每一个字形来作为一个独立实体。另请注意,两条竖线被支持用来表示0宽度的制表符。


 

 

制表符延展的处理是直接的。所有我们需要做的就是修改width-list中的个别制表符的width-条目。一旦这么做了,所有的绘制和鼠标命中测试都将使用修改后的字形宽度(glyph-widths),

从而导致那些制表符的地方有额外的空间被分配。

 

制表符延展显然发生在ScriptShap和ScriptPlace被调用之后,这个处理方法,UspAnalyze是调用另一个内部函数来实现-ExpandTabs:

 

BOOL ExpandTabs(USPDATA *uspData, WCHAR *wstr, int wlen, SCRIPT_TABDEF *tabdef);

 

SCRIPT_TABDEF是一个标准的Uniscribe结构体,它被用于ScriptStringAnalyze中。 它包含字符串tab-stops的信息 (大小和位置)。 我在UspLib中用同样的结构体纯粹是为了一致性。

 

应用属性

UspLib支持为Unicode文本字符串指定可变长度的attribute-runs样式,它使用ATTR结构数组。 尽管Neatpad不利用这个工具(它只是每个ATTR设置为“1”单元长),指定变长runs的可能性仍然存在。


 

 

虽然这不是它自身的问题,但处理可变长度的style-runs同时来显示字形run会变得非常复杂。为了简化这个问题UspLib总是平坦地为任何使用者提供attribute-run,并保持在USPDATA对象内一个内部副本。平坦的run-list被分配了与原始Unicode字符串相同的长度,而且包含每个原始 Unicode 字符恰好一个 ATTR 结构。

 

UspApplyAttributes(USPDATA *uspData, ATTR *attrRunList)

 

UspApplyAttributes(上面)用于更新属于一个USPDATA对象的style-run 信息,它是UspAnalyze分析字符串处理步骤的一部分。但是这个函数能够在一个字符串被分析后的任何时候被调用。注意,后续调用UspApplyAttributes后只有颜色信息被更新了– 如果重新应用字体信息的话,还将需要再次分析整个字符串。

 

UspAnalyze

我们已经用足够的覆盖面完成了UspAnalyze的实现。所有分析阶段的相关代码都在UspLib.c文件中。所有的函数功能如下所示。


 

 

所有这些工作的结果就是一个USPDATA对象,其中包含了显示一个Unicode文本字符串所有必要的信息。

 

typedef struct _USPDATA

{

  //

  // Item-run information

  //

  int              itemRunCount;

  ITEM_RUN       * itemRunList;

  int            * visualToLogicalList;

 

  //

  // Logical character/cluster information (1 unit per original WCHAR)

  //

  int              stringLen;            // length of current string (in WCHARs)

  WORD           * clusterList;          // logical cluster info

  ATTR           * attrList;             // flattened attribute-list

 

  //

  // Glyph information for the entire paragraph

  // Each ITEM_RUN references a position within these lists:

  //

  int              glyphCount;           // count of glyphs currently stored

  WORD           * glyphList;

  int            * widthList;

  GOFFSET        * offsetList;

  SCRIPT_VISATTR * svaList;

 

  //

  // external, user-maintained font-table

  //

  USPFONT        * uspFontList;

 

} USPDATA, *PUSPDATA;

 

上表中详细介绍了USPDATA结构。为了清楚起见,我省略了那些不需要讨论的‘辅助’字段。

 

处理Uniscribe的主要困难之一就是知道如何处理产生的这些大量信息。在UspLib中,我采取的策略是保持所有的信息到USPDATA对象内部。“Per-run”的字形信息连接在几个大型的缓冲区中(glyphList, widthList 等),每个ITEM_RUN 使用ITEM_RUN::glyphPos 和 ITEM_RUN::glyphCount字段在每个这些大型缓冲区中指明数据范围。

 

基本上有两个途径可用于Uniscribe –可归结为速度 vs 内存的消耗。第一种策略就是将所有的信息聚集到一个对象里。这具有操作快速的优势,因为’分析’阶段(itemization, shaping 等)仅发生了一次。之后字型数据将被存储起来,然后每次文本需要显示时,都可以重复使用它。

 

另一个途径就是为了节省内存,只有在需要的时候才分配缓冲区,每次要求字形信息时,都要再次调用 ScriptShape/Place。优点前面已经提过,但缺点是性能上的损失。每次它们显示的时候都要重新整形item-runs,这将是非常低效的– 考虑到一个文本编辑器在每次改变鼠标选择时都将需要重绘,那么这个策略是我想避免的。

 

对于UspLib我选择了速度优先(资源重)来实现。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值