Txt引擎工作原理

前言

最近为了解决切章过程中碰到的各种疑难杂症,不得不深入到TxtEngine中调试分析一些细节,尽管之前为了解决卡牌的排版问题也接触过,但是每次都是只见树木不见森林。为了避免一叶障目不见泰山,所以下决心梳理下该阅读引擎,从整体上了解引擎的工作原理,包括打开文件、排版、渲染、翻页等环节。

打开文件

获取文件大小和文件流

1
2
3
4
5
6
7
// 给定文件路径,获取文件大小。
_fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] fileSize];

// 获取文件流(如果文件较大,需要分批次读入)
unsigned long readSize = (_fileSize < QR_MAX_GUESS_LEN ? _fileSize:QR_MAX_GUESS_LEN);
NSData* data = [_fileHandle readDataOfLength:readSize];
unsigned char* buff = (unsigned char*)[data bytes];

编码检测

在对文件流解析之前,需要确定其编码类型。那么如何判断一个文件的编码类型呢 ?

有BOM文件的识别

BOM(Byte Order Mark),字节序标志,通常在文件的最开头,添加额外的几个无效字节,用来专门说明该文件是什么类型编码的文件,有的情况下BOM还带着大小端的信息。读取到文件的开头几个字节,就能明确该用什么样的编码方式解析文件流。

常见的UTF编码方式有以下这些,其中UTF-16和UTF-32还有大小端之分

1
2
3
4
5
FF FE 00 00 : UTF-32/UCS-4, 小端
00 00 FE FF : UTF-32/UCS-4, 大端
FF FE : UTF-16, 小端
FE FF : UTF-16, 大端
EF BB BF : UTF-8 没有大小端之分

为什么UTF-8没有大小端的问题呢?这是因为如果一个字符使用utf-8表示,就需要将这个字符的Unicode码,编码成字节数组,所以对于字节数组写入内存时,只需要按照数组的顺序,一个一个字节写入,不存在高位和低位的问题。 所以,一个汉字在任何类型的CPU中生成的utf-8序列是一样的。

另外,在微软Window操作系统中,会给utf-8文件添加BOM【EF BB BF】(虽然不需要这么做),这并不是说明UTF-8需要字节序,而是仅仅表名该文件是utf-8编码的文件。有的情况下这样多余的动作有时候反而会带来额外的麻烦,所以在编码判定过程中也应该将这样的情况考虑在内,切莫把BOM作为该文件开头正文的一部分。

到此为止,打开一个文件,如果看到有BOM,就能通过识别BOM,来判断该文件的编码是UTF-16、UTF-32,UTF-8,同时判断是大端还是小端。

无BOM文件的识别

对于没有BOM的文件,又该如何判定它的编码类型呢?
这里拿UTF-8和GBK举例说明(GBK编码没有BOM,UIF-8允许含BOM,但通常没有BOM)

先看UTF-8:
UTF-8(Unicode Transformation Format-8bit)是用以解决国际上字符的一种多字节编码,它对英文使用8位(即一个字节),中文使用24为(三个字节)来编码。UTF-8包含全世界所有国家需要用到的字符,是国际编码,通用性强。它是一种多字节编码的字符集,表示一个Unicode字符时,它可以是1个至多个字节,在表示上有规律:

1
2
3
4
5
6
1字节:0xxxxxxx 
2字节:110xxxxx 10xxxxxx
3字节:1110xxxx 10xxxxxx 10xxxxxx
4字节:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5字节:111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6字节:1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

通过识别头部,就能知道以几个字节为单位进行解析,并且头部只可能出现这几种情况,之后的每个字节均是以10开头的,规律非常明显。 这样就可以根据上面的特征对字符串进行遍历来判断一个字符串是不是UTF-8编码了。

再看GBK:

GBK是国家标准GB2312基础上扩容后兼容GB2312的标准。GBK的文字编码是用双字节来表示的,即不论中、英文字符均使用双字节来表示,为了区分中文,将其最高位都设定成1。GBK编码就是基于区位码的,用双字节编码表示中文和中文符号。一般编码方式是:0xA0+区号,0xA0+位号。例如汉字:“安”,区位号是1618(十进制),那么“安”字的GBK编码就是 0xA0+16 0xA0+18 也就是 0xB0 0xB2。虽然是双字节显示,但是GBK的每个字节的取值也是有范围的,GBK总体编码范围为0x8140~0xFEFE,首字节在 0x81~0xFE 之间,尾字节在 0x40~0xFE 之间,剔除 xx7F 一条线。

通过以上,我们可以看出虽然GBK和UTF-8的文件没有标记,但是他们的编码结果都是有鲜明特征的,我们可以很容易的通过查看文件字节序列来判断该文件是GBK还是UTF-8编码的。

然而这样也带来了一定的性能消耗:必须从头到尾检测每一个字符是否都在某种编码正常的规则范围之内,例如下面的代码中,全文检测每个字符是否符合UTF-8规范。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
bool isUTF8( const char *str, int length )
{
int i, mode = 0, m2;
unsigned long ucs;
char ch;

for( i = 0 ; i < length - 2 ; i++ ){
ch = str[ i ];
if( mode == 0 )
{
if( (ch&0x80) == 0 ) continue;
if( (ch&0xe0) == 0xc0 ){ mode = m2 = 1; ucs = (ch&0x1f); continue; }
if( (ch&0xf0) == 0xe0 ){ mode = m2 = 2; ucs = (ch&0x0f); continue; }
if( (ch&0xf8) == 0xf0 ){ mode = m2 = 3; ucs = (ch&0x07); continue; }
if( (ch&0xfc) == 0xf8 ){ mode = m2 = 4; ucs = (ch&0x03); continue; }
if( (ch&0xfe) == 0xfc ){ mode = m2 = 5; ucs = (ch&0x01); continue; }
return false;
}
else
{
if( (ch&0xc0) != 0x80 ) return 0;
ucs <<= 6; ucs += (ch&0x3f);
mode--;
if( !mode ){
if( m2 == 1 && ucs < 0x0000080 ) return 0;
if( m2 == 2 && ucs < 0x0000800 ) return 0;
if( m2 == 3 && ucs < 0x0010000 ) return 0;
if( m2 == 4 && ucs < 0x0200000 ) return 0;
if( m2 == 5 && ucs < 0x4000000 ) return 0;
}
}
}
return true;
}

获取到文件的编码格式、字节序等信息后,确定一些无意义的字节_docBOMSize

排版设置

  • 设置显示区域
  • 设置字体类型和字体大小
  • 设置行高、行间距、段间距

区域、行间距、段间距都比较简单,这里重点说明下字体、行高、字宽的问题

字体注册

如果是系统自带的字体,那么这里无需做什么,然而对于不是内置的字体,首先要加载字体并注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 加载字体
NSString *fontFilePath = [self getFontFilePath:fontType];
NSString *url = [NSURL fileURLWithPath:fontFilePath];
CGDataProviderRef fontDataProvider = CGDataProviderCreateWithURL((CFURLRef)url);
CGFontRef newFont = CGFontCreateWithDataProvider(fontDataProvider);
CGDataProviderRelease(fontDataProvider);

// 注册字体
CTFontManagerRegisterGraphicsFont(newFont, NULL);

// 获取CTFontRef
CTFontRef ctFont = CTFontCreateWithGraphicsFont(newFont, fontSize,NULL,NULL);
return ctFont;

字宽计算

TxtEngine中涉及到的主要字符:ASCII字符、CJK字符和一些标点符号,对于每一种字体,在某个行高下,都需要去计算对应的字宽

  • 目前QQ阅读的字体有多种选择:除了自带的系统黑体,还提供了很多其它可供下载的字体:汉仪书宋、汉仪旗黑、汉仪楷体、方正兰亭、方正书宋、方正卡通、方正启体、方正行黑等,因为有字体注册功能,所以这里可以继续扩展新的字体。

  • 对于每一种字体,TxtEngine支持的字体高度介于10~55

1
2
#define CacheFontSizeMin               10  
#define CacheFontSizeMax 55
  • 对于每一种字体,在每一个行高下,都需要额外计算并缓存相应的字宽(懒加载方式)
1
2
3
unsigned char       _bak_ascii_width[CacheFontSizeMax-CacheFontSizeMin+1][256][20];     // 字体大小|ASCII字符|字体类型
unsigned char _bak_wchar_width[CacheFontSizeMax-CacheFontSizeMin+1][20]; // 字体大小|字体类型
unsigned char _bak_biaodian_width[CacheFontSizeMax-CacheFontSizeMin+1][18][20]; // 字体大小|标点字符|字体类型

ASCII字符宽度

1
2
3
4
5
6
7
8
// cache visible character width
for ( i = 0; i < 256; ++i )
{
if ( _bak_ascii_width[nFontSize - CacheFontSizeMin][i][nFontType] == 0xff )
{
_bak_ascii_width[ nFontSize - CacheFontSizeMin][i][nFontType] = font_character_width([_setting parseFont], (QWCHAR)(i));
}
}

font_character_width会根据当前的字体类型确定是否是系统内置字体,如果不是,还需要找到字体文件进行加载注册,最后将字体对应的CTFontRef返回,然后根据字体的CTFontRef和字符计算宽度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int font_string_width_byCTFont(CTFontRef ctFont, NSString* uniChar)
{
UniChar *characters;
CGGlyph *glyphs;
CFIndex count = [uniChar length] ;//1;

// Allocate our buffers for characters and glyphs.
characters = (UniChar *)malloc(sizeof(UniChar) * count);
//assert(characters != NULL);

glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * count);
//assert(glyphs != NULL);

// Get the characters from the string.
CFStringGetCharacters((CFStringRef)uniChar, CFRangeMake(0, count), characters);

// Get the glyphs for the characters.
CTFontGetGlyphsForCharacters(ctFont, characters, glyphs, count);

// Do something with the glyphs here, if a character is unmapped
CGSize *cSize = (CGSize *)malloc(sizeof(CGSize)*count);

// 英文字体可能会有小数,所以这里做四舍五入,以免英文字体挤压。
int fontwidth = (int) (0.5 + CTFontGetAdvancesForGlyphs(ctFont, kCTFontDefaultOrientation, glyphs, cSize, count) );

// Free our buffers
free(characters);
free(glyphs);
free(cSize);

return fontwidth;

}

设置汉字宽度

中日韩文字(CJK)宽度基本上是固定的,因此只需要计算某一个字符的宽度并缓存下来,而不是每次都计算一个字的宽度,这样可以加快排版速度。这里font_character_width_2的第二个参数就是取了CJK的第一个unicode 0x4E00,用来代表所有的CJK符号的宽度。

1
2
3
4
5
6
7
8
9
10
(chr >= 0x4E00 && chr <= 0x9FBF)            // 4E00-9FBF:CJK 统一表意符号 (CJK Unified Ideographs)
(chr >= 0xF900 && chr <= 0xFAFF) // F900-FAFF:CJK 兼容象形文字 (CJK Compatibility Ideographs)
(chr >= 0xFE30 && chr <= 0xFE4F) // FE30-FE4F:CJK 兼容形式 (CJK Compatibility Forms)
(chr >= 0xFF00 && chr <= 0xFFEF) // FF00-FFEF:半型及全型形式 (Halfwidth and Fullwidth Form)全角标点、全角ASCII
(chr >= 0x3200 && chr <= 0x32FF) // 3200-32FF:封闭式 CJK 文字和月份 (Enclosed CJK Letters and Months)
(chr >= 0xAC00 && chr <= 0xD7AF) // 韩文拼音
(chr >= 0x1100 && chr <= 0x11FF) // 韩文字母
(chr >= 0x3130 && chr <= 0x318F) // 韩文兼容字母
(chr >= 0x3040 && chr <= 0x309F) // 日文平假名 (Hiragana)
(chr >= 0x30A0 && chr <= 0x30FF) // 日文片假名 (Katakana)

1
2
3
4
5
6
// cache chinese character
int nChineseWidth = font_character_width_2([_setting parseFont], (QWCHAR)0x4E00 , nFontType);
if ( _bak_wchar_width[nFontSize - CacheFontSizeMin][nFontType] == 0xff )
{
_bak_wchar_width[nFontSize - CacheFontSizeMin][nFontType] = nChineseWidth;
}

设置标点符号宽度

排版过程中用到的标点符号(这里是全角,半角的应该包含在ASCII字符中)主要包括以下类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 标点符合类型
static unsigned short biaodian_array[] =
{
U_PUNCT_COMMA, // 0xFF0C | ,
U_PUNCT_FULLPOINT, // 0x3002 | 。
U_PUNCT_QUESTION, // 0xFF1F | ?
U_PUNCT_SEMICOLON, // 0xFF1B | ;
U_PUNCT_EXCLAMATION, // 0xFF01 | !
U_PUNCT_COLON, // 0xFF1A | :
U_PUNCT_PAUSE, // 0x3001 | 、
U_PUNCT_LEFTSINGLEQUOTE, // 0x2018 | ‘
U_PUNCT_RIGHTSINGLEQUOTE, // 0x2019 | ’
U_PUNCT_LEFTDOUBLEQUOTE, // 0x201C | “
U_PUNCT_RIGHTDOUBLEQUOTE, // 0x201D | ”
U_PUNCT_LEFTBOOKQUOTE, // 0x300A |《
U_PUNCT_RIGHTBOOKQUOTE, // 0x300B | 》
U_PUNCT_LEFTBRACKET, // 0xFF08 |(
U_PUNCT_RIGHTBRACKET, // 0xFF09 | )
U_PUNCT_CONNECT, // 0x002D | -
U_PUNCT_PERCENT, // 0x0025 | %
U_PUNCT_SPACER, // 0x00B7 | .
U_PUNCT_POZHE, // 0x2014 | 破折号——
U_PUNCT_SHENGLUE, // 0x2026 | 省略号
U_PUNCT_LEFTANCIENTEQUOTE, // 0x300C | 古体左引号「
U_PUNCT_RIGHTANCIENTEQUOTE // 0x300D | 古体右引号」
};

对于这些标点符号,会计算每一个标点符号在某种字体大小条件下的宽度,并进行缓存,计算过程和前面计算ASCII的过程一致

1
2
3
4
5
6
7
8
9
// cache biaodian character
int nLen = sizeof( biaodian_array ) / sizeof ( unsigned short) ;
for ( i = 0; i < nLen; ++i )
{
if ( _bak_biaodian_width[ nFontSize - CacheFontSizeMin][i][nFontType] == 0xff )
{
_bak_biaodian_width[ nFontSize - CacheFontSizeMin][i][nFontType] = font_character_width_2([_setting parseFont], (QWCHAR)(biaodian_array[i]) , nFontType);
}
}

排版

在打开文件之后,一般都会从一个锚点进入,锚点包含2部分:段落偏移和行偏移。

分段

如果锚点所在的段落链表尚未建立,便会触发段落解析逻辑。分段逻辑是根据这几个标识符来进行的:【\n\r】【\n】【\r】【\r\n】,每次都会从当前的数据流中找到下一个标识符,如果找到了,就可以根据前后两个偏移值确定一个新段落的偏移和大小

1
2
3
4
5
- (BOOL) NextBreaker:(unsigned char *) buffcountCharInWidth
buffSize:(unsigned long) buff_size
buffOff:(unsigned long )buff_off
brkOff:(unsigned long * )brk_off
skipBytes:(unsigned long * )skip_bytes

以UTF-8格式为例:

1
"第1章 下辈子也不会爱上你\r\n6月6日,星期六。\r\n    这是一个十分吉利的日子,更是一个黄道吉日。\r\n    这一日,是京城有名的富家之女梁贝璇与其男友曹瑞年的婚礼,婚礼早就被各路媒体公布出去了,好多人的眼睛都在看着这一个家室及其不匹配的婚姻,所有人都报以怀疑的态度,只是没有人会主动说出来。\r\n    此时的梁贝璇坐在宽敞明亮、奢华至极的房间内,穿着洁白定制的婚纱,脸上带着甜蜜的梦幻般的笑容。\r\n    只是这样的笑容在她收到一个快递之后就彻底的失去了......\r\n

这是一部分章节内容,如何进行分段呢?当然是从前往后一次遍历,逐个寻找\r\n,直到结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
bool qr_utf8_next_breaker( unsigned char * buff, unsigned long buff_size, unsigned long * brk_off, unsigned long * skip_bytes )
{
bool found = false;
unsigned long offset;

for ( offset = 0; offset < buff_size; ++offset )
{
// 如果已经到了二进制流的结尾,跳出循环。结尾判定条件:buff[offset] == 0x00
if ( qr_is_end_char( buff[offset] ) )
break;

// 如果找到\r,分为两种情况:\r\n和\r
if ( 0x0D == buff[offset] ) // \r
{
*brk_off = offset;

if ( (offset + 1 < buff_size) && (0x0A == buff[offset + 1]) )
{
*skip_bytes = offset + 2; // \r\n
}
else
{
*skip_bytes = offset + 1; // \r
}

found = true;
break;
}
// 如果找到\n,分为两种情况:\n\r和\n
else if ( 0x0A == buff[offset] ) // \n
{
*brk_off = offset;

if ( (offset + 1 < buff_size) && (0x0D == buff[offset + 1]) )
{
*skip_bytes = offset + 2; // \n\r
}
else
{
*skip_bytes = offset + 1; // \n
}

found = true;
break;
}
}

// 边界判定
if (offset >= buff_size)
{
found = false;
*skip_bytes = buff_size;
}

return found;
}

这样一次遍历后,便会找到一个段落的开始偏移和结束偏移

获取段落字数

拿到一个段落对应的字节流之后,需要确定其对应的字符个数

1
2
3
- (BOOL)    CharCount:( unsigned char * )buff
buffSize:(unsigned long) buff_size
charCount:(unsigned long * )cnt

还是以UTF-8举例,一般情况下,utf8每一个字可能有1到3各字节

1
2
3
4
1字节:0xxxxxxx  0000~0111 8种情况都是1个字节
2字节:110xxxxx 1100和1101 索引为12和13的两种情况都是2个字节
3字节:1110xxxx 1110,只有1种情况,是3个字节
0字节:其它的都是0个字节

有了上面的基础,就可以用一个数组__bpc存放字节数,取utf8首字节的高4位,转换为索引,直接获取对应的字节数

1
2
3
4
5
6
7
8
9
static const unsigned long __bpc[16] =
{
1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 3, 0
};

int qr_utf_char_bytes(unsigned char ch)
{
return (int)(__bpc[ch >> 4]);
}

有了上面的铺垫,就可以快速计算某一段字节流对应的字符数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
unsigned long   qr_utf8_charcnt( unsigned char * buff, unsigned long buff_size )
{
unsigned long chr_cnt;
unsigned long offset;
unsigned long chr_bytes;

if ( NULL == buff || 0 == buff_size )
return 0;

for ( chr_cnt = 0, offset = 0; offset < buff_size; ++chr_cnt )
{
if ( qr_is_end_char( buff[offset] ) )
break;

chr_bytes = qr_utf_char_bytes( buff[offset] );//
if ( chr_bytes == 0 )
break;

if ( offset + chr_bytes > buff_size )
break;

offset += chr_bytes;
}

return chr_cnt;
}

生成段落结构体

TxtEngine里面有2种段落结构体,分别是段文本__struct_TextPrgf_Text和段属性__struct_TextPrgf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct __struct_TextPrgf_Text        TextPrgf_Text_t;
typedef struct __struct_TextPrgf_Text * TextPrgf_Text_ptr_t;

//段文本数据结构
struct __struct_TextPrgf_Text
{
TextPrgf_t link; //段属性
unsigned long line_count;
TextLine_ptr_t first_line; // [strong ref]
TextLine_ptr_t last_line; // [strong ref]

unsigned short *wide_buff; // Paragraph text buffer (UCS2 encoding)
unsigned long wide_size; // Size of paragraph text buffer
QRRect *char_rects; // character drawrect array
unsigned long *charactersOffsets; // 段内每个字相对文件起始位置的字节offset
long charactersCountAddOne; // 该段中字的个数加1
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct __struct_TextPrgf            TextPrgf_t;
typedef struct __struct_TextPrgf * TextPrgf_ptr_t;

//段属性数据结构
struct __struct_TextPrgf
{
TextPrgf_ptr_t prev; // [weak ref]
TextPrgf_ptr_t next; // [weak ref]

unsigned long type; // QR_TEXT_PRGF_TYPE_XXX
unsigned long attrib; // QR_TEXT_PRGF_ATTR_XXX
unsigned long offset; // Offset in current charpter //..file
unsigned long size; // Paragraph size in charpter //file

unsigned long page_index;
unsigned long charpter_index;
//章节尾页模型的序号
int tail_index;
int need_remove_para_space;//0不需要。1需要
};

值得注意的是:段文本的第一个字段link正好是段属性类型,所以给定一个段文本结构体:

1
__struct_TextPrgf_Text textPrgf_Text;

可以直接强制转换

1
__struct_TextPrgf textPrgf = (__struct_TextPrgf)textPrgf_Text;

前面解析了段落的开始和结束偏移,并且计算出了字符数,确定了需要申请的内存大小,此时创建段落:

1
2
3
4
//申请空间
last_prgf = qr_alloc_prgf_text( chr_count+2 );//add two space ericni
last_prgf->need_remove_para_space = 0;
last_prgf->tail_index = -1;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
TextPrgf_ptr_t qr_alloc_prgf_text( unsigned long char_cnt )
{
TextPrgf_Text_ptr_t prgf = NULL;

if ( 0 == char_cnt ) return NULL;

prgf = malloc(sizeof(TextPrgf_Text_t));

long charactersCountAddOne = char_cnt + 1;

prgf->link.prev = NULL;
prgf->link.next = NULL;
prgf->link.type = QR_TEXT_PRGF_TYPE_TEXT;
prgf->link.attrib = 0;
prgf->link.offset = 0;
prgf->link.size = 0;

prgf->wide_buff = NULL;
prgf->char_rects = NULL;
prgf->wide_size = QR_ROUND_UP_4( (charactersCountAddOne << 1) );

prgf->line_count = 0;
prgf->first_line = NULL;
prgf->last_line = NULL;

prgf->wide_buff = malloc(prgf->wide_size);

// alloc memory for char_rects
prgf->char_rects = malloc(charactersCountAddOne * sizeof(QRRect));
memset(prgf->char_rects, 0, charactersCountAddOne * sizeof(QRRect));

prgf->charactersOffsets = malloc(charactersCountAddOne * sizeof(unsigned long));
memset(prgf->charactersOffsets, 0, charactersCountAddOne * sizeof(unsigned long));

prgf->charactersCountAddOne = charactersCountAddOne;

return (TextPrgf_ptr_t)prgf;
}

计算字符偏移

计算每个字相对文件的字节offset

1
2
3
4
5
[_endcoder countCharactersOffset:&_readCache[prgf_offset]
characters:chr_count
buffSize:prgf_size
baseOffset:file_offset + prgf_offset
characterOffsets:text_prgf->charactersOffsets];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- (BOOL)_utf8CountCharactersOffset:(unsigned char *)buff
characters:(unsigned long)characters
buffSize:(unsigned long)buffSize
baseOffset:(unsigned long)baseOffset
offsets:(unsigned long *)offsets
{
if (NULL == buff || 0 == buffSize || offsets == NULL) {
return NO;
}

unsigned long characterOffset = baseOffset;

// characterIndex < characters 防止offsets越界
for (unsigned long characterIndex = 0, offset = 0;offset < buffSize && characterIndex < characters;++characterIndex) {

if (qr_is_end_char(buff[offset])) {
break;
}

*(offsets + characterIndex) = characterOffset;

// utf8每一个字有1到3各字节的可能性
//
unsigned long characterBytes = qr_utf_char_bytes(buff[offset]);

if (characterBytes == 0) {
break;
}

if (offset + characterBytes > buffSize) {
break;
}

offset += characterBytes;

// 下一个字的偏移
//
characterOffset += characterBytes;
}

return YES;
}

编码转换

将字符编码转成Unicode,并存在&text_prgf->wide_buff[0]

1
2
3
4
5
6
- (BOOL)    EncodeUCS2:( unsigned short *) dst_buff
destSize:(unsigned long )dst_size
srcBuff:(unsigned char *) src_buff
srcSize:(unsigned long) src_size
convLen:(unsigned long * )conv_len
usedBytes:(unsigned long * )used_bytes

这里以utf8转换为unicode为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
unsigned long qr_utf8_to_ucs2( unsigned short * buff, unsigned long buff_size, unsigned char * utf8_str, unsigned long utf8_len,unsigned long* used_utf8_len )
{
unsigned long conv_len = 0;
unsigned long char_cnt = 0;
unsigned long char_bytes;
unsigned char * us_utf8_str = (unsigned char*)utf8_str;
unsigned long i;
unsigned short tmp_ch = '\0';

if ( NULL == buff || NULL == us_utf8_str || buff_size < 2 || 0 == utf8_len )
return 0;

*used_utf8_len = 0;

if ( utf8_len >= 3 && (us_utf8_str[0] == 0xEF && us_utf8_str[1] == 0xBB && us_utf8_str[2] == 0xBF) )
{
// skip BOM
us_utf8_str += 3;
*used_utf8_len += 3;
}

for ( i = 0; i < utf8_len; ++i )
{
if ( '\0' == *us_utf8_str ) break;

char_bytes = __utf8_bpc[((unsigned char)*us_utf8_str) >> 4];


if ( 1 == char_bytes )
{
tmp_ch = (unsigned short)us_utf8_str[0];
*used_utf8_len += char_bytes;
}
else if ( 2 == char_bytes )
{
if ( '\0' != us_utf8_str[1] )
{
tmp_ch = ((unsigned short)(us_utf8_str[0] & 0x1F) << 6) | ((unsigned short)(us_utf8_str[1] ^ 0x80));
*used_utf8_len += char_bytes;
}
}
else if ( 3 == char_bytes )
{
if ( '\0' != us_utf8_str[1] && '\0' != us_utf8_str[2] )
{
tmp_ch = ((unsigned short)(us_utf8_str[0] & 0x0F) << 12) | ((unsigned short)(us_utf8_str[1] ^ 0x80) << 6) | ((unsigned short)(us_utf8_str[2] ^ 0x80));
*used_utf8_len += char_bytes;
}
}
else
{
*used_utf8_len += 1;
}

if ( char_cnt >= ((buff_size / 2) - 1) )
break;

buff[char_cnt] = tmp_ch;
char_cnt += 1;


conv_len += 2;
us_utf8_str += char_bytes;
}

buff[char_cnt] = '\0';

return conv_len;
}

确定段落类型

为了确定段的具体类型,需要对段落类型进行识别

  • 判断一个段落是否为一个章节的标题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (BOOL)isPrafChapterTitle:( TextPrgf_Text_ptr_t) text_prgf group:(TextPrgfGroup_ptr_t)groupPtr
{
if (_isLocal || groupPtr == NULL) {
return NO;
}

BOOL isTitle = NO;
if (groupPtr->head == NULL && groupPtr->tail == NULL && text_prgf->link.offset == 0 && _offset <= 4) {
//用于目录跳转和从书架打开时 group这时为空链表时判断是否为标题
isTitle = YES;
}
else if (_offset <= 4 || groupPtr->head != NULL)
{
//用户relayout 这时group已经有链表了
if (( TextPrgf_Text_ptr_t)groupPtr ->head == text_prgf && text_prgf->link.offset == 0) {
isTitle = YES;
}
}
else if (_offset > 4)
{
//看书看到章节中间 然后退到书架再进入阅读页的情况
if (groupPtr->head == NULL && groupPtr->tail == NULL && text_prgf->link.offset == 0) {
isTitle = YES;
}
}
return isTitle;
}
  • 该段落是否为作者的话,作者的话可以有多个段落,每个段落都以特殊字符”\u3000\u2029\u3000”结尾。另外,作者的话的第1段以”\u2029\u3000\u2029”开始
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 是否是作者的话
- (BOOL)isAuthorTipsWithParagraphFirstPtr:(unsigned char *)firstPtr endPtr:(unsigned char *)endPtr
{
if (strlen((const char *)endPtr) < QRAuthorTipsBeforeEndMarkLength || _isLocal)
{
return NO;
}

// 作者的话以特殊字符"\u3000\u2029\u3000"结尾
return (endPtr[0] == 0xe3 && endPtr[1] == 0x80 && endPtr[2] == 0x80 &&
endPtr[3] == 0xe2 && endPtr[4] == 0x80 && endPtr[5] == 0xa9 &&
endPtr[6] == 0xe3 && endPtr[7] == 0x80 && endPtr[8] ==0x80);
}

// 是否是作者的话中的第一段
- (BOOL)isFirstParagOfAuthorTipsWithFirstPtr:(unsigned char *)firstPtr
{
if (strlen((const char *)firstPtr) < QRAuthorTipsBeforeEndMarkLength || _isLocal)
{
return NO;
}

// 作者的话第一段以"\u2029\u3000\u2029"开始
return (firstPtr[0] == 0xe2 && firstPtr[1] == 0x80 && firstPtr[2] == 0xa9 &&
firstPtr[3] == 0xe3 && firstPtr[4] == 0x80 && firstPtr[5] == 0x80 &&
firstPtr[6] == 0xe2 && firstPtr[7] == 0x80 && firstPtr[8] == 0xa9);
}
  • 该段落是否为”(本章完)”,这个是判断ptr指向的内容是否是:”(本章完)\r\n”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (BOOL)isParagraphEndStr:(unsigned char *)ptr
{
if (!ptr || _isLocal || strlen((const char *)ptr) != 13) {
return NO;
}

// 判断ptr指向的内容是否是:"(本章完)\r\n"
//
return *ptr == '(' &&
*(ptr + 1) == 0xe6 && *(ptr + 2) == 0x9c && *(ptr + 3) == 0xac && // "本"utf8
*(ptr + 4) == 0xe7 && *(ptr + 5) == 0xab && *(ptr + 6) == 0xa0 && // "章"utf8
*(ptr + 7) == 0xe5 && *(ptr + 8) == 0xae && *(ptr + 9) == 0x8c && // "完"utf8
*(ptr + 10) == ')' &&
*(ptr + 11) == '\r' &&
*(ptr + 12) == '\n';
}

段落分行

对于前面提到的段落拆分、编码转换以及字符偏移计算等,只需要执行1次即可,之后便无需更新,因为这部分信息是相对独立的,没有依赖其它的布局条件。然而,将段内文本根据当前字体、方向等条件进行布局会依赖相关设置,例如绘制区域大小、横竖屏、字体大小、字体颜色、行间距、段间距等,一旦其中的某一个设置信息发生变化,那么这里的布局也需要更新。

布局的本质:根据排版设置,将一个段落生成逐行的排版结果,需要注意的是,这里不会包含高度偏移信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 这里的代码省略了对标题的判断逻辑
- (BOOL)relayoutTextPrgf:(TextPrgf_Text_ptr_t) text_prgf midOff:(unsigned long) mid_off group:(TextPrgfGroup_ptr_t)groupPtr

{
//TODO: 排一段
unsigned long ret_val;
unsigned long width;
unsigned long length;
unsigned long bound_w;

unsigned long index;
unsigned long max_len;
unsigned long offset;
TextLine_ptr_t text_line;
QRFont* doc_font;
QRFont* doc_title_font = _titleSetting.font_obj ;

if ( NULL != text_prgf->last_line )
{
offset = text_prgf->last_line->offset;
if ( offset + text_prgf->last_line->length >= text_prgf->wide_size )
return NO;
}

doc_font = _setting.font_obj;
if (QR_TEXT_PRGF_TYPE_AUTHOR == text_prgf->link.type ||
QR_TEXT_PRGF_TYPE_BUSINISS_TEXT == text_prgf->link.type) {
doc_font = [self authorTipsSetting].font_obj;
}

max_len = text_prgf->wide_size / 2;
bound_w = _docBoundRc.w;

index = 0;

//先对0到mid_off的字符进行分行
if ( mid_off > 0 )
{
for ( offset = 0; offset < mid_off; offset += length, ++index )
{
ret_val = [doc_font countCharInWidth:&text_prgf->wide_buff[offset] rect:&text_prgf->char_rects[offset] length:(mid_off - offset) maxWidth:bound_w outWidth:&width count:&length alignmeng:0];

if ( QR_OK != ret_val || 0 == length ) break;

//20151023 lucas 添加 英语、数字断行不切断单词
qr_relayout_english_2(&text_prgf->wide_buff[offset] , mid_off - offset , 0, &length);

text_line = qr_alloc_line();
if ( NULL == text_line ) break;

text_line->index = index;
text_line->offset = offset;
text_line->width = width;
text_line->length = length;
text_line->isCharpterTitle = 0;
qr_push_back_line( text_prgf, text_line );
}
}

//再从mid_off到结尾的字符进行分行
for ( offset = mid_off; offset < max_len; offset += length, ++index )
{
ret_val = [doc_font countCharInWidth:&text_prgf->wide_buff[offset] rect:&text_prgf->char_rects[offset] length:(max_len - offset) maxWidth:bound_w outWidth:&width count:&length alignmeng:0];

if ( QR_OK != ret_val || 0 == length ) break;

// 20151023 lucas 添加 英语、数字断行不切断单词
qr_relayout_english_2(&text_prgf->wide_buff[offset] , max_len - offset , 0, &length);

text_line = qr_alloc_line();
if ( NULL == text_line ) break;

text_line->index = index;
text_line->offset = offset;
text_line->width = width;
text_line->length = length;

text_line->isCharpterTitle = 0;
qr_push_back_line( text_prgf, text_line );
}

return YES;
}

先来解决第一个问题:这里为什么会引入mid_off呢?主要是因为有些段落可能会跨页,在这种情况下,一旦发生转屏,那么当前页面会继续从开始锚点重新排版,而跨到上一页的段落的最后一行可能无法排满一行,所以这里引入了这样的规则:跨到上页段落的最后一行只排版到锚点的前一个字符,这样前一页的最后一行就可能会发生断行现象。而对于没有跨页的段落,mid_off自然是0,不会执行前半部分逻辑

分行的核心在于计算其中每个字符的宽度,并根据排版页面的宽度决定某个字符是否是当前行的最后一个字符,从而生成段落中的一行
另外,除了正常的计算过程外,还有一些额外的规则:

(1)过滤掉emoji表情字符
(2)标点符号避头尾规则:

  • 1、当前行行尾是左引号,左括号,左书名号,左单引号 (单双),破折号,省略号 ,移到下一行
  • 2、下一行连续两个标点符号,当前行行尾不是标点符号,将当前行的最后一个字符移到下一行,避免标点出现在行首
  • 3、下一行行首只有一个标点符号,将该标点符号往上一行移

(3)行尾逗号、句号算一半宽度
(4)每排版完一行,最后需要将空格考虑在内,重新计算字符的x值和宽度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
- (unsigned long)countCharInWidth:(unsigned short*)buff
rect:(QRRect*)char_rects
length:(unsigned long)len
maxWidth:(unsigned long)max_width
outWidth:(unsigned long*)pwidth
count:(unsigned long*)char_cnt
alignmeng:(unsigned int)text_align
{
unsigned long i;
unsigned long t;
signed long width = 0;
unsigned long count = 0;
double space_all;
double space_each;
bool break_line = false;

if ( nil == _setting || NULL == buff || NULL == pwidth || NULL == char_cnt )
return QR_FAILED;


if(char_rects == NULL)
{
for ( i = 0; '\0' != buff[i] && i < len; ++i )
{
// 过滤掉emoji表情字符
if(buff[i]>=0xe000&&buff[i]<=0xe5ff)
buff[i] = 0x0020;//空格

t = [self getCharWidth:buff[i]];

if ( width + t > max_width ) {
break;
}

width += t;
count += 1;
}
*pwidth = width;
*char_cnt = count;

[self reLayoutSkipHeadPunct:buff length:(unsigned)len count:char_cnt width:pwidth];

return QR_OK;
}

for ( i = 0; '\0' != buff[i] && i < len; ++i )
{
// 过滤掉emoji表情字符
if(buff[i]>=0xe000 && buff[i]<=0xe5ff) buff[i] = 0x0020;//空格

t = [self getCharWidth:buff[i]];

// 判断是否已经满了一行
if ( width + t > max_width )
{
break_line = true;
break;
}

(char_rects+i)->x = width;
(char_rects+i)->w = t;

width += t;
count += 1;
}
*pwidth = width;
*char_cnt = count;

// 文字不够一行,直接返回
if (!break_line) return QR_OK;

// 标点符号避头尾规则:
// 1、当前行行尾是左引号,左括号,左书名号,左单引号 (单双),破折号,省略号 ,移到下一行
// 2、下一行连续两个标点符号,当前行行尾不是标点符号,将当前行的最后一个字符移到下一行,避免标点出现在行首
// 3、下一行行首只有一个标点符号,将该标点符号往上一行移
[self reLayoutSkipHeadPunct:buff length:(unsigned)len count:char_cnt width:pwidth];

// 行尾增加了一个标点
if(*char_cnt>count)
{
(char_rects+*char_cnt-1)->x = width;
(char_rects+*char_cnt-1)->w = *pwidth - width;
}

// 行尾逗号、句号算一半宽度
if(buff[*char_cnt-1] == U_PUNCT_COMMA||
buff[*char_cnt-1] == U_PUNCT_FULLPOINT||
buff[*char_cnt-1] == U_PUNCT_PAUSE)
{
(char_rects+*char_cnt-1)->w /= 2;
*pwidth -= (char_rects+*char_cnt-1)->w;
}

// 将空格考虑在内,重新计算字符的x值和宽度
space_all = (double)max_width - (*pwidth);
space_each = space_all/(*char_cnt);

for (i = 0; i < *char_cnt ; i++)
{
(char_rects+i)->x += (space_each*i);
if(i > 0)
{
(char_rects+i-1)->w = (char_rects+i)->x - (char_rects+i-1)->x;
}
}
(char_rects+i-1)->w = max_width - (char_rects+i-1)->x;//last character width can't be forget


return QR_OK;
}

对于英文文章,还有一些特殊的排版规则:有些英文字符过长,可能某一行的最后一个单词(数字)到行尾还没有排版完,为了避免断行不切断单词(数字),需要遵守一些规则:

  • 如果下一行的开始字符不是英文字符(数字),这说明当前行完全可以容纳最后一个单词,属于正常情况,直接返回;
  • 如果下一行的开始字符是数字或者英文字符,那么就需要查看当前行最后一个字符是否是字符或者数字
    如果是,需要统计当前行的最后一个单词(数字)长度,如果单词(数字)的长度有限,那么将该单词(数字)的前半部分移到下一行,否则不做处理; 如果不是,不做处理;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
unsigned long qr_relayout_english_2(unsigned short * buff, 
unsigned long len,
unsigned long offsetX,
unsigned long * char_cnt )
{
signed long i;
unsigned long t;
unsigned long count = 0;

if ( NULL == buff || NULL == char_cnt ) return QR_FAILED;

i = *char_cnt;

if (i >= len || '\0' == buff[i] ) return QR_FAILED;

// 如果下一行的开始字符不是数字或英文字符,这说明当前行完全可以容纳最后一个单词,属于正常情况,直接返回
if (qr_is_english_digist_letter(buff[i]) == QR_FAILED) // lucas 1224
return QR_FAILED;

// 否则如果下一行的开始字符是数字或者英文字符,那么就需要统计当前行的最后一个单词长度
t = 0;
for ( i = (*char_cnt) - 1; i >= 0 && '\0' != buff[i] ; --i )
{
if (qr_is_english_digist_letter(buff[i]) == QR_FAILED)
if (qr_is_skip_punct_letter_tail_forbid(buff[i]) == QR_FAILED) {
break;
}
t++;
}
if (i < 0 && offsetX == 0 ) return QR_FAILED;

// 将单词的前半部分挪到下一行
count = *char_cnt;
count -= t;
*char_cnt = count;

// 如果追溯到行首,并且单词的字符个数已经超过了10个,但是单词仍然没有结束,此时还原回去,不做处理
if(*char_cnt == 0 && t > 10)//连续英文超过10个就断开 ericni 2012.10.09
*char_cnt += t;

return QR_OK;
}

插入段处理

业务层要插入到排版里面的数据

  • 有的需要插入到段落中间,例如神想法,而有的需要插入到段落尾部,例如章尾的各种业务数据:文字广告、图片广告、QA问答、卡牌、广点通数据等等。
  • 这些插入的段落中有的需要向正文一样进行排版,例如作者的话,其排版规则和正文是一致的,除了字体、字体大小和颜色可能不同,而有的仅仅是需要占位,占位大小由业务层决定,引擎在排版完成后,需要将占位的坐标传递给业务层,让业务层在排版预留的占位区域内完成业务逻辑
1
2
3
4
5
6
TextPrgf_ptr_t custom_prgf = qr_alloc_prgf_space();
if (custom_prgf) {
custom_prgf->type = QR_TEXT_PRGF_TYPE_CUSTOM;
custom_prgf->size = 0;
custom_prgf->tail_index = i;
}

插入逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
- (void)_insertPrgf:(TextPrgf_ptr_t)dstPrgf
beforePrgf:(TextPrgf_ptr_t)srcPrgf
atPrgfGroup:(TextPrgfGroup_ptr_t)prgfGroup
{
if (dstPrgf == NULL ||
srcPrgf == NULL ||
prgfGroup == NULL) {
return;
}

if (srcPrgf == _currPage.beginPrgf &&
_currPage.beginLine.ref != NULL &&
_currPage.beginLine.ref->prev == NULL) {
_currPage.beginPrgf = dstPrgf;
[self setFirstLine:_currPage.beginPrgf];
}

_nextPage.isValid = NO;
_prevPage.isValid = NO;

if (srcPrgf->prev == NULL) {
if (srcPrgf != prgfGroup->head) {
// 说明src不在prgfGroup中
//
return;
}

dstPrgf->prev = NULL;
dstPrgf->next = srcPrgf;
srcPrgf->prev = dstPrgf;
prgfGroup->head = dstPrgf;
}
else {
dstPrgf->prev = srcPrgf->prev;
dstPrgf->next = srcPrgf;
srcPrgf->prev = dstPrgf;
dstPrgf->prev->next = dstPrgf;
}

prgfGroup->count += 1;
}

渲染

有了排版过程的各种前期准备,渲染过程就相对简单了:
(1)准备好画布、画笔、颜料
(2)找到当前页的首段落指针和首行指针;
(3)确定好行高、行间距、段间距;

逐行根据排版得到每个字符的x值和w值绘制字符,直到当前画布无法容纳为止;
这里的渲染过程省略了一些代码,方便流程梳理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
- (BOOL)repaint:(QRCanvas *)canvas rect:(QRRect *)rect
{
// 各种数据准备
...

// 开始绘制
[canvas drawTextBatchStart:0 length:0 destinationRect:0 alignment:align_flag];

// 设置高度区域
unsigned int yOffset = _docBoundRc.y;
unsigned int yOffsetMax = _docBoundRc.y+_docBoundRc.h-_fontHeight;

// 设置绘制区域
[self recalc];
draw_rect.x = _docBoundRc.x;
draw_rect.y = yOffset;
draw_rect.w = _docBoundRc.w;
draw_rect.h = _fontHeight;

// 设置画笔颜色
UIColor *lineColor = [QRThemeColorWithKey(kGlobalQRReadViewTextSelectionMainColor) getValue];

for (; yOffset < yOffsetMax && NULL != p_curr_prgf;)
{
if (QR_TEXT_PRGF_TYPE_SPACE == p_curr_prgf->type) {
// 空段不处理
}
else if (QR_TEXT_PRGF_TYPE_TEXT == p_curr_prgf->type ||
QR_TEXT_PRGF_TYPE_AUTHOR == p_curr_prgf->type ||
QR_TEXT_PRGF_TYPE_BUSINISS_TEXT == p_curr_prgf->type)
{
// 获取某段落开始绘制的行
text_prgf = (TextPrgf_Text_ptr_t)p_curr_prgf;

if (p_curr_prgf == _currPrgf) {
p_curr_line = _currLine.ref;
}
else {
p_curr_line = text_prgf->first_line;
}

if (text_prgf->line_count <= 0) {
continue;
}

unsigned int curLineH = 0;
QRSetting *fontSetting = nil;

// 逐行绘制
for (; yOffset < yOffsetMax && NULL != p_curr_line;) {
draw_rect.y = yOffset;

if (QR_TEXT_PRGF_TYPE_AUTHOR == p_curr_prgf->type ||
QR_TEXT_PRGF_TYPE_BUSINISS_TEXT == p_curr_prgf->type) {
fontSetting = [self authorTipsSetting];
curLineH = [self authorTipsLineHeight];
[canvas setContentFont:[fontSetting getFontSize:FontState_Render] Type:[fontSetting getFontType:FontState_Render]];
[canvas setPenColor:textcolor];
}
else {
curLineH = _lineHeight;
[canvas setContentFont:[_setting getFontSize:FontState_Render] Type:[_setting getFontType:FontState_Render]];
fontSetting = _setting;
[canvas setPenColor:textcolor];
}

QRFont *currentFont = fontSetting.font_obj;
unsigned int currentFontHeight = 0;
[currentFont getHeight:&currentFontHeight];
draw_rect.h = currentFontHeight;

//画一行文字
[canvas drawTextBatchDoing:(text_prgf->wide_buff + p_curr_line->offset)
inRect:(QRRect *)(text_prgf->char_rects + p_curr_line->offset)
length:p_curr_line->length
destinationRect:(QRRect *)&draw_rect
alignment:align_flag];

// 增加偏移值
yOffset += curLineH;

// 和高亮区间有重合就画高亮,全文搜索高亮显示会走到这里(代码省略)
...

// 段评气泡绘制(代码省略)
...

// 指向下一行
p_curr_line = p_curr_line->next;
}

// 段间距偏移
if (p_curr_prgf->need_remove_para_space == 1) {
yOffset -= _lineSpace/2;
}else{
yOffset += _paraSpace;
}

}
else if (QR_TEXT_PRGF_TYPE_BOTTOMMENU == p_curr_prgf->type) {
// 底部4tab入口绘制(代码省略)
}
else if (QR_TEXT_PRGF_TYPE_EXCELLENTPARACOMMENT == p_curr_prgf->type) {
// 神想法绘制(代码省略)
}
else if (QR_TEXT_PRGF_TYPE_CUSTOM == p_curr_prgf->type) {
// 自定义模块(代码省略)
}
else {
break;
}
// 指向下一段
p_curr_prgf = p_curr_prgf->next;
}

// 结束绘制
[canvas drawTextBatchEnd:0 length:0 destinationRect:0 alignment:0];

...

return YES;
}

这里最后的真正绘制还是系统的方法:

1
[text drawAtPoint:CGPointMake(x, y) withAttributes:@{NSFontAttributeName : gc->fontBatch, NSForegroundColorAttributeName : textColor}];

有一些额外的信息需要注意:
(1)绘制高亮区域,例如有些笔记划线,所以每绘制完一行,都要去进行判断;
(2)在每个文字段的结尾判断是否需要绘制段评气泡;
(3)中间遇到一些定制的业务信息,根据给定的高度预留位置;

翻页

当前页已经渲染完成,此时如果想移动到下一页或者上一页,就需要寻找下一页的开始锚点或者上一页的开始锚点。TxtEngine主要用到了下面的几个方法:

1
2
3
4
- (BOOL)moveNextPage;
- (BOOL)movePrevPage;
- (BOOL)isTop;
- (BOOL)isBottom;

前两个方法的逻辑和渲染方法的逻辑大概是一致的,只是整个过程中仅仅计算Y的偏移,而不进行绘制
(1)moveNextPage:根据当前页的锚点,逐行排版到当前页的结尾,然后找到下一页的开始段落偏移和行偏移;
(2)movePrevPage:根据当前页的锚点,倒着排版到上一页的开始,找到上一页的开始段落偏移和行偏移;

这里有几个缺陷:

  • 当前页已经渲染完成,寻找下一页的开始锚点只需要根据当前页的尾指针就可以很快定位,无需大循环跑一遍,遗憾的是,尽管很早之前就引入了TxtPage,里面就包含尾指针,却未能利用起来;这也是斯雨目前正在努力做的事情;
  • 判断是否是当前章的结尾isBottom,也是只需要根据尾指针进行判断,遗憾的是,isBottom也在跑着大循环圈;
  • 无论是哪个方法,每次跑完循环,都不做任何缓存,导致大圈会执行很多次;
  • 前面这两个循环和渲染的循环存在着一定的误差,有时候会造成段落丢失或者重复排版的现象。

如果能用TxtPage记录下当前页面的开始指针和结束指针,一方面保证每一页的排版和渲染过程只走1次,那么可以去除很多没有必要的性能消耗,另一方面保证渲染和排版的结果是一致的,省去很多烦人的排版问题

1
2
3
4
5
6
@interface TxtPage : NSObject
@property (nonatomic, assign) TextPrgf_ptr_t beginPrgf;
@property (nonatomic, assign) LinePos_t beginLine;
@property (nonatomic, assign) TextPrgf_ptr_t endPrgf;
@property (nonatomic, assign) LinePos_t endLine
@end

段落缓存

前后翻页的时候往往首先需要判定当前内存中构建的段落是否足够构建上一页或者下一页:
(1)有的情况下,章节文件较大,可能无法一次性将章节文件全部读入,
(2)有的情况下,从章节中间的某个锚点进入,此时只加载当前锚点之后的数据流,此时往前翻页的时候,就需要加载锚点之前的数据流,构建之前的段落。

这里的判定是设置了一个阈值:cache_bytes = 8 * 1024

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
*功能:从上往下可用的缓存是否足够
*/
- (BOOL)enoughUp2down:(TextPrgf_ptr_t)from_prgf cacheBytes:(unsigned long)cache_bytes
{
if(NULL == from_prgf) {
return NO;
}

if (NULL == _prgfGroup.tail) {
return NO;
}

// 如果已经是最后一段,offset标识
if (_prgfGroup.tail->offset + _prgfGroup.tail->size >= _fileSize) {
return YES;
}

// 如果已经是最后一段,类型标识
if (_prgfGroup.tail->type == QR_TEXT_PRGF_TYPE_BOTTOMMENU) {
return YES;
}

// 判断最后一段到当前段落之间的offset差距是否大于 8 * 1024
long long temp_size = (long long)(_prgfGroup.tail->offset + _prgfGroup.tail->size) - (long long)(from_prgf->offset + from_prgf->size);
return temp_size >= (long long)cache_bytes;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
*功能:从下往上可用的缓存是否足够
*/
- (BOOL)enoughDown2up:(TextPrgf_ptr_t)from_prgf cacheBytes:(unsigned long)cache_bytes
{
unsigned long temp_size;

if (NULL == _prgfGroup.head) {
return 0;
}

if(NULL == from_prgf) {
return 0;
}

// 如果当前段落已经是首段,说明之前已经没有数据,无需再缓存
if (_prgfGroup.head->offset <= _docBOMSize) {
return 1;
}

// 判断当前段落的偏移到首段偏移之间的差距是否大于 8 * 1024
temp_size = from_prgf->offset - _prgfGroup.head->offset;
return (temp_size >= cache_bytes ? 1 : 0);
}

新加载的缓存会立刻被解析为段落链表,包括段落拆分、编码转换、及字符偏移等,并且将段内文本根据当前字体、方向等条件进行布局。最后将新缓存解析的结果prgf_group merge 到总的段落组中_prgfGroup

1
qr_merge_front_prgf( &_prgfGroup, &prgf_group );

在线书的章节文件普遍都比较小,某个章节对应的段落链表大部分情况下都会被缓存到内存中,然而碰到文件比较大的情况时(例如导入书),段落链表就不能一直缓存到内存中,需要根据当前阅读的偏移值,前后缓存一定距离的段落,在这个距离之外的段落就需要清除掉。
具体的逻辑可以参考:

1
2
- (void) recycleFromTop:(unsigned long) recycle_size;
- (void) recycleFromBottom:(unsigned long )recycle_size;

通用思路

对于在线书,其章节文件本身就比较小,完全可以尝试下新的思路:将章节文件内容一次性全部读入,段落拆分、编码转换、分行,然后从头到尾根据当前设置进行分页,并将分页结果缓存下来,每次渲染直接拿页码去获取页面的开始指针和结束指针直接绘制,这样就不会存在正翻和倒翻的区别了,更不会存在段落丢失或者重复排版的现象

断章

何为断章?有的文件中可能包含多个章节,并且除了该文件外,没有其它任何信息,这样就需要根据文件内容确定章节信息(章节个数,每个章节标题、内容和起始偏移等)。而确定的过程就是寻找”第xxx章(章节回卷篇部)”的过程,而寻找这些字符的过程和前面寻找\n\r的过程是极其类似的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static const unsigned short    __charset_spc[] =
{
// (半角空格,TAB,全角空格)
0x0020,0x0009,0x3000
};
static const unsigned short __charset_1st[] =
{
// 第
0x7B2C
};
static const unsigned short __charset_2nd[] =
{
// 零一二三四五六七八九十百千零壹贰叁肆伍陆柒捌玖拾佰仟0123456789
0x96F6,0x4E00,0x4E8C,0x4E09,0x56DB,0x4E94,0x516D,0x4E03,0x516B,0x4E5D,
0x5341,0x767E,0x5343,0x96F6,0x58F9,0x8D30,0x53C1,0x8086,0x4F0D,0x9646,
0x67D2,0x634C,0x7396,0x62FE,0x4F70,0x4EDF,0x0030,0x0031,0x0032,0x0033,
0x0034,0x0035,0x0036,0x0037,0x0038,0x0039
};
static const unsigned short __charset_3rd[] =
{
// 章节回卷篇部
0x7AE0,0x8282,0x56DE,0x5377,0x7BC7,0x90E8
};

总结

文章尚有诸多不足,后续完善

-------------本文结束 感谢您的阅读-------------

本文标题:Txt引擎工作原理

文章作者:lingyun

发布时间:2019年04月05日 - 23:04

最后更新:2020年12月27日 - 12:12

原始链接:https://tsuijunxi.github.io/2019/04/05/Txt引擎工作原理/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!

本文标题:Txt引擎工作原理

文章作者:lingyun

发布时间:2019年04月05日 - 23:04

最后更新:2020年12月27日 - 12:12

原始链接:https://tsuijunxi.github.io/2019/04/05/Txt引擎工作原理/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。