文档一致性测试系统的研究与设计
2022-05-27师子源王明飞
师子源, 李 成, 王明飞
(1. 北京印刷学院,北京 102600; 2. 奥博杰天软件(北京)有限公司,北京 100022)
个性化的文档输出在越来越多的行业中被应用。 比如,以前去银行或保险公司去办理业务时,所填写的表格往往是预印制表单,用户或工作人员在留空中填写相关的内容。 而现在,得益于动态文档生成系统的发展,越来越多的传统印制表单被动态文档所代替。 动态文档生成系统彻底改变了文档产生方式,之前静态文档的制作过程就变成了利用文档模板和用户数据进行动态文档生成的过程。
动态文档生成过程通常可以分为以下四步。
(1)制作动态文档模板。 动态文档生成系统会提供广泛的文档开发工具的插件,用户可以利用这些插件在文档内部插入相应的变量或规则。
(2)内容组装。 根据特定的用户数据,替换文档模板中的变量;计算文档模板定义的规则,将生成的内容添加进文档模板。
(3)文档排版。 根据生成的内容和文档模板中的文档样式定义进行排版,该部分功能称为排版引擎,而排版引擎确保输出精确、紧凑的排版结果。
(4)文档生成。 文档排版完成后,系统就会根据定义好的输出方式生成对应格式的静态文档,目前,使用最多的格式是便携式文档格式(Portable Document Format,简称PDF)。
从上述动态文档的产生过程步骤描述可以知道,动态文档从制作到最终发布是一个非常复杂的过程。 动态文档生成系统本身作为一个复杂软件系统,会有软件系统所固有的问题(Software Bugs),并且会不断进行产品功能升级。 解决这些问题或功能升级过程中的一个很小的改动,最终输出文档可能会发生很大变化。
而对于用户来讲,只要文档模板和用户数据不变,最终生成的静态文档就要始终保持一致。 输入数据一致的情况下,系统迭代更新过程中新版本生成的文档必须要和旧版本已经生成的文档保持一致。 只有这种一致性能够得到保证,客户才会放心地把他们现有系统中的数据迁移到新版本系统中。通常,使用动态文档生成系统的用户都会有数量极其庞大的文档数据,靠人工去验证两个版本系统生成文档之间的差异已经不现实。
为了解决上面提出的问题,需要设计一个能够自动化批量比较文档的系统。 该系统能够自动比较两份文档,自动生成比较结果报表,并且自动发送通知给相关人员。 同时,该系统应该是一个轻量级的系统,提供多种运行方式,既可以方便地集成到客户现有的测试系统中,也可以单独运行,方便开发人员在运行、提交代码时检查会不会产生输出文档的变化。
综上所述,确保在动态文档生成系统的不断迭代过程中整个系统最终生成文档的一致性是在系统开发、升级迭代过程中的重要内容。
高丽萍[1]等人对Word 中图文混排的文档一致性的控制做了研究,解决了图文混排的一些冲突情况。 马康[2]在结构化文档的自动化提取方面做了研究。 牛永洁[3]等人做了基于PDFBox 的信息提取方面的研究与实现。 朱玲玉[4]在文档解析与文本内容提取及脱敏方面做了较深入研究。 徐俊刚[5]等人用本地文件模拟数据库的方法绕过原系统对数据库的依赖,提出一个自动化测试方案。
本文通过对PDF 文档的解析,提取文档中的文本、图像、图形及表单并分类,再分别与原文档中的对应内容相比较,完成一致性测试。 系统基本框架还包括与外部集成模块、结果报表生成模块和结果发送模块,系统结构图如图1 所示。
图1 文档一致性测试系统结构图
1 PDF 文档解析
PDF 格式是一种独立于应用程序、硬件、操作系统呈现文档的文件格式,适用于邮件、归档等发布需求。 每个PDF 文件包含固定布局的平面文档的完整描述,包括文本、字形、图形、图像及其他需要显示的信息。 在对PDF 文档比较之前,首先要分析PDF 文档的基本结构和文档内容的组织方式。
1.1 PDF 文档结构
一个PDF 文件从大的方面来说可以分4 个部分:文件头(Header)、文件体(Body)、交叉引用表(XRef Table)、文件尾(Trailer),参考文献都有很详细的论述[3][4][6],其逻辑结构如图2 所示,从Trailer 开始进行解析可以逐步定位到页面组对象(Pages),进而进行页面内容提取。
图2 PDF 文档的逻辑结构图
PDF 的页面基本内容包括存储为字符串的文本内容、由图形和线条组成的矢量图形、由照片和其他类型的图片组成的位图、链接(文档内部或网页)、表单、JavaScript 等。 页面内容结构图如图3所示,Resources 指示了PDF 资源对象,包含字体、图形、图像、表格等;Fonts 指示字体信息,XObjects包含了图片、表格等;Contents 指PDF 页面内容对象,包含PDF 页面内的路径、文字、内嵌图像数据等页面描述信息,PDF 规范里都有详细描述。
图3 PDF 页面内容结构图
系统选择PDF 格式作为比较文档格式来实现一致性测试系统,其中基准文件称为BaselinePDF,对比文件称为TestPDF。 PDF 文档比较模块需要对页面的所有内容作比较,并记录所有变化。 PDF格式是非结构化的文档,并且PDF 文档的页内容是按照定点描述的,任意内容可以输出在任意位置,这就导致程序不能以流的方式对文档页内容进行从上而下的比较。 将文档内容分为文本、图像、图形、表单之后进行分类比较是易于实现的,需要比较的分类内容如表1 所示。
表1 PDF 页面比较内容
系统以分类比较的方式进行一致性测试,其基本流程图如图4 所示。 首先对输入的Baseline 和Test 文件进行解析,提取内容生成中间数据;然后将数据分为文本、图形、图像、PDF 表单四类;依次对四类内容进行比较,最后生成比较结果报表。
图4 文档一致性测试系统流程图
1.2 PDF 页内容提取
系统采用了开源PDF 库Apache PDFBox 作为PDF 文件的基础解析模块。 但PDFBox 解析PDF的结果数据并不适用于PDF 的分类比较,需要利用PDFBox 的API 生成本系统所需要的数据。
研究PDFBox 中关于PDF Rendering 部分的接口,可以发现所有解析PDF 页的类都继承于抽象函数PDFGraphicsStreamEngine,本系统的实现也应遵守PDFBox 的接口规范和实现规则,设计一个符合本系统需求的解析类,该类应继承并实现PDFGraphicsStreamEngine。 该实现类完成下列功能:
(1)解析PDF 页中的所有内容。
(2)对分析出来的内容进行归类,分为文本,图形图像,表单等单元。
(3)记录所有单元出现的位置及区域信息,方便比较结果的展示。
(4)记录所有单元在绘制时的绘制参数, 即当前图形状态(Graphics State)。
1.2.1 解析页面内容
设计一个继承PDFBox 定义的抽象类PDFGraphicsStreamEngine 的实现类PDFContentExtractor,实现该抽象类中所定义的抽象方法(Abstract Method),完成内容解析和分类功能。
(1)解析PDF 页中的所有内容。
以解析一段文本内容举例说明如何实现抽象类PDFGraphicsStreamEngine。 经过对源码分析,文本中内容的开始结束都有对应的抽象接口,在实现类PageContentExtractor 中需要通过实现这些抽象方法,对于文本内容,其对应的抽象方法是abstract void beginText()和abstract void endText()。
beginText()标志解析流程开始处理文本内容,得到一个新的TextContent,压入堆栈pageContentStack。 endText()标志解析流程结束一段文本内容,将beginText()处生成的TextContent 对象从pageContentStack 中弹出,将TextContent 对象加入页内容列表PageContentList 中。
使用了一个堆栈pageContentStack 来检查当前解析内容的开始结束是否配对。 在beginText 处往堆栈中压入一个TextContent 对象,那么在endText处从堆栈中弹出的对象类型必须是TextContent,否则说明解析过程的解析内容产生了不一致。
同样,对于图像内容实现beginImage( ) 和endImage()方法,对于表单对象应实现beginAnnot()和endAnnot()方法。
页内容全部解析完成后,所有内容对象(Page-Content)都存储于队列pageContentList 中,Page-Conent 的继承类对应了其类型,比如TextContent,ImageContent 等。
(2)记录所有单元出现的位置及区域信息。
由于在比较模块的需求中还需要比较对应内容的位置及大小,所以解析过程中还需要计算各个PageContent 的轮廓(Outline)信息。
这里仍然以文本内容为例,在PageContentExtractor 中重载(Override) 了方法Protected void drawGlyph2D(),本方法负责得到一个字符的字形(Glyph)。
在drawGlyph2D 方法内,得到Glyph 的同时,记录Glyph 的BoundingBox 信息,添加到TextContent 对象。 一个TextContent 对象可能包含多个字符,只需连接所有字符的BoundingBox 信息,就可以得到最终这个TextConent 的轮廓大小。
(3)记录每个内容单元在绘制时的绘制参数。
在每个内容单元的绘制过程中,都有对应的当前绘制状态,包括字体、颜色、变换矩阵等。 程序在记录内容的同时,在每个PageContent 对象内部记录当前的Graphics State。
完成解析工作之后,系统将会得到一个以PDF页为单元的完整中间数据集合。
1.2.2 PDF 页内容分类
中间数据集合包含了PDF 文件的所有内容,对数据集进行分类并生成各类数据集以便于比较。设计比较器的输入数据类PageThread,PageThead分析提取的内容队列pageContentList,生成用于比较的四个内容集合:文本集合(TextThread)、图像集合(ImageSet)、图形集合(PathSet)、表单集合(AnnotSet)。
(1)文本
提取PDF 文档中每一页所包含的文本内容,对页内的所有文本内容按照文本位置从上到下,从左到右的方式进行排序。 排序完成后,所有文本内容形成一个文本串,根据文本样式的变化将文本进行分段。 针对每一段文本,记录文本内容,样式表等相关信息,其中样式信息包含字体、字号、颜色、颜色空间等,最终生成TextThread。
文本对比在本系统中是以单词作为基本比较单元。 但PDF 文本内容描述中不以单词作为基本单元,是以单个字符出现,只需要字符位置正确,PDF 打开后就可以得到正确的页面展示。 所以在建立一个TextThread 的时候,还要加入智能分词的功能。 本系统的基本分词算法是:按照位置相近的原则将文本分区域,将页内的所有文本分块,假定临近的文本是处于同一段落;然后对每一区域内的文本以空格和标点符号分割,形成以词为基本单元的结构。 这样处理的好处是复杂度低,易于实现;不足之处是无法形成更有效的结构,比如不能知道文本是否在同一段落,或者不能知道文本是否在表格中。
(2)图形
提取PDF 文档中每一页所包含的图形内容,记录包括图形路径数据、画笔模式、填充模式等,生成PathSet。
(3)图像
提取PDF 文档中每一页所包含的图像内容,记录图像的特征参数,包括图像大小、图像格式、图像位数、颜色空间等信息,生成ImageSet。
(4)PDF 表单
PDF 表单在PDF 文件结构里面是独立于页面描述的,除了分析提取表单本身属性信息,包括表单类型、表单名称、表单提示等,还需要分析提取表单内容(Appearence)部分。 根据PDF 格式对表单的定义,表单内容可以包括任意文本、图形图像,因此表单内容部分包含以上三个部分的集合。 同时提取表单属性及表单内容生成AnnotSet。
2 PDF 内容比较
2.1 文本内容比较
对TextThread 进行字符串比较(TextDiff)采用的是经典Diff 算法最长公共子序列算法LCS(Longest Common Subsequence)。 LCS 最长公共子序列算法的基本原理是:用一个矩阵来记录两个字符串中所有位置的两个字符之间的匹配情况,若是匹配则为1,否则为0。 然后求出对角线最长的1 的序列,其对应的位置就是最长匹配子串的位置。
LCS 算法的开源实现有很多,系统选用了Google 的google-diff-match-patch 实现,该算法用Java 实现,性能优秀,非常适合用于本系统需要的文本对比功能。
TextDiff 结束后,如果TextDiff 的结果为相同,则取当前相同子字符串的位置,生成一个字符块(TextLob)。 如果TextDiff 的结果为不相同,同样生成一个TextLob,此处不相同的字符块包含下列三种情况:
#INSERT:TestPDF 中的文本在BaselinePDF 中未找到。
#DELETE:BaselinePDF 中的文本在TestPDF中未找到。
#DIFFRENCE:TestPDF 中的文本在BaselinePDF 对应位置的文本不相同。
经过对TextDiff 的比较,页内的文本被划分成为了一系列字符块的集合。 对于内容相同的TextLob,比较他们的样式,发现不同则记录为一条不同内容(DiffContent);对于内容不相同的TextLob,直接记录一条DiffContent。
2.2 图像比较
要实现对PDF 页内的图像进行比较,首先取图像属性信息进行比较,对包括图像大小、图像字节数、图像格式、像素位数、色彩空间等参数直接进行比较,如果发现任何一项不同,则直接记录一条
DiffContent。
如果以上所有比较都相同,则需要进一步对比图像的实际内容。 首先,利用图像数据生成对应的Java2D BufferedImage 对象,然后对两个BufferedImage 对象中的像素进行逐一比对,发现不相同的像素则记录一条DiffContent。
2.3 图形比较
将PDF Graphics Path 转换成Java2D General Path,然后利用Java2D Graphcis API 进行比较。 对于比较结果不相同的图形,记录一条DiffContent,同时记录当前的Graphics State。
图形比较部分只实现了直线(Line)的精确比较。 对于曲线图形,采用将曲线转成图像的方式进行图像像素对比,如果图像对比结果不相同,则曲线不相同。 这种方法好处是容易实现;不足之处是无法指出曲线到底哪部分不一致。
2.4 PDF 表单比较
PDF 表单可以分为自身属性部分和表单展现内容部分,要实现对PDF 表单的比较,同样需要进行这两部分的比较。
对于表单自身属性部分,可以直接比较每一个属性值,发现不同,则直接记录一条DiffContent。
对于表单内容部分,可能包括任意文本、图形、图像,所以表单比较器内部就需要包含上述的文本、图形、图像比较器,比较后得到所有不相同的记录。 如果记录条数不为零,则记录一条DiffContent,该DiffContent 可能包含表单内容比较结果中的多个DiffContent。
根据内容单元比较功能需求,设计一个页比较的功能类PageContentComparator,该类内部需要包含上述的四个内容比较器。 内容比较器可以抽象一个父类ContentCompartor,在该父类中定义一个抽象方法compare 方法,上述四个比较器都应该是ContentComparator 的子类。 在每个子类中实现compare 方法,在该方法中完成比较功能。
3 测试及结论
针对本系统的特性,尤其针对PDF 文档比较部分的特点,选取有代表性的PDF 文件作为输入文件Baseline。 利用PDF 生成库通过编程方式对该文件做改变,生成测试文件Test,包括损坏文件、相同文件、删除一段文字、增加一段文字、改变字体、颜色、大小、插入页、插入空白页、增加图像、删除图像、改变图形等。 以此测试用例的设计思路,建立了完善的测试用例集合。 经过测试,系统生成文件可以明确指出文档的区别和位置。
通常一个标准的动态内容文档生成系统每天都会生成大量的文档,这就对本系统的性能提出了很高的要求。 因此,在进行系统功能测试的同时,还对本系统进行了相关的性能测试。 分别以多页PDF 文件、多个多页PDF 文件、文字密集型多个多页PDF 文件作为测试文件,测试系统比较一份PDF 文件所需时间,得出了比较理想的测试结果,平均处理一个PDF 标准页的时间大约在100ms 以内;最极端的内容密集型PDF 页的平均处理时间大约在300ms 以内。 完全能够达到一个标准动态内容文档生成系统的测试要求。
虽然测试系统得到了有效的验证,但是在测试方法上还有很多需要改进的地方。 如文本提取中分词算法可以先采用OCR 预处理来判断内容结构;文本对比是基于英文字符实现的,如果是中文字符需要进一步研究;图形比较中对直线以外的图形处理方式需要做进一步研究。