首页 技术研究 读书心得 作品 思考 教育信息化
绿色通道: Java Web开发中的中文问题 OpenGL入门教程 MacRhine CHMReaderWM5 教学理论简介 PDFBox中文支持
自己也做个广告 :-)
承接有意思的软件项目,目前特别感兴方向有:
Mac OS X 驱动   作品: ViaRhine Driver for MacOSX
OpenGL ES 应用
包含 EJB 的 J2EE 应用
基于 JSF 的WEB应用
FileSystem Driver (Windows & Linux)
编译器及代码分析工具
教育信息化咨询、自适应测试平台、联机教学环境

pinxue (at) hotmail.com

解决PDFBox不能读取中文PDF问题

PDFBox是一个相对简洁实用的处理PDF文件的Java类库。PDFBox具有丰富的功能,但我只关心利用它来抽取PDF文件中的文本,这也非常简单,PDFBox作者已经考虑到这种用法并提供了很好的支持,这是PDFBox相对iText对我更具吸引力的重要原因,另外它的体积也比iText小多了。

不过PDFBox处理中文文件有问题,google了一下,抱怨的一堆,解决方案没找着,还是自己动手解决吧。还好,不是太复杂,在小黑屋里关了一天就找到了原因了。以下就是寻找解决方案过程的记录。

PDF Box的总体结构

1Mysoo使用PDF Box涉及三个层次

PDFTextStripper ==> PD Model ==> COS Model

COS Model是对PDF文件格式的直接映射,PD Model则是对COS ModelOO封装,其关系如下图所示:


上半部份是PDModel,每个PDModel对象都是对于特定COSModel对象的封装,下半部份是COSModel,每个COSModel对象都直接对应PDF文档的一段内容。

PDFTextStripper是一辅助工具,它封装了通过PDModel取得文档的所有页面,并从每一页可能包含有文本的对象中取出字符的操作。

2PDF Box取出文本的处理过程

  1. PDFTextStripper.writeText()

  2.   For each PDPage of PDDocument
  3.     ProcessPage()
  4.       PDFStreamEngine.processStream() in the page
  5.         Parse cosStream to get operator name and its parameters
  6.         Delegate to operator retrieved from stream
  7.           => SetGraphicsStateParameters operator parse GraphicStates
  8.           => ShowText operator handle COSString
  9.             Retreive byte[] from COSString
  10.             PDFStreamEngine.showString(bytes[])

文本处理的实际内容就在 PDFStreamEngine.showString() 里,在这之前 SetGraphicsStateParameters已经分析并保存了PDF的当前绘制格式参数,其中与文本处理相关的就是字体PDFontShowString()会利用这个PDFont对源自PDF文件的字节串进行解码,形成对应的字符串。

3PDFStreamEngine的字符解码过程

PDFStreamEngine.showString(byte[] string)

    for each byte in input string

        firtly try PDFont.encode( string[offset], 1 byte )

        if return null, then try PDFont.encode( string[offset], 2 bytes)

            PDFont.encode( bytes, count ), convert bytes into a single char string

                PDFont try to get CMap

                    if the font has embbed TO_UNICODE Cmap then parse it

                    else retrieve Cmap based on ENCODING attribute of the font

// 测试中cn.pdfType0字体,encodingName GBK-EUC-H

// GBK-EUC-H 代表 MS CP936 (lfCharSet 0x86), GBK charset, GBK encoding

// 第一块文本处理时,cmapObjects为空,

PDFont perform cmap name subtitution

PDFont parseCmap from Resource/cmap/cmapName (GBK-EUC-H)

CmapParser.parse(resStream) 分析并创建cmap实例

cmap对象注册到cmapObjects {encodingName, cmap}

                   如果1字节字符识别返回null,则尝试2字符

cmap.lookup( bytes, count )

如果字符表里没找到

PDFont.getEncoding()

    从font ENCODING属性中取得encoding (GBK-EUC-H)

         EncodingManager.getEncoding( encoding )

                Manager从内部ENCODINGS表中名字代表的Encoding对象

       这张表是在Mangerstatic块里初始化的

            加ENCODINGS.put( COSName.GBK_EUC_H_ENCODING, new GbkEucHEncoding() );

PDFont.getCodeFromArray(bytes, count)

        ''-1字节时应该返回0xC7,两字节时应该返回0xC7B0

Encoding.getCharacter(code)

如果Encoding也处理不了就getStringFromArray()


faint! PDFont:

protected int getCodeFromArray( byte[] data, int offset, int length )

{

int code = 0;

for( int i=0; i<length; i++ )

{

code <<= 8;

code = (data[offset+i]+256)%256; //这行应该是 |=

}

return code;

}


// 注:PDFont.java 395~401if分支的代码貌似多余

// cmap = (CMap)cmapObjects.get( encodingName );

// if( cmap != null )

// {

// cmap = (CMap)cmapObjects.get( encodingName );

// }

// else

// { ... }


PDF文件中字体描述部份:

0020de0: 626a 0d3c 3c20 0d2f 5479 7065 202f 466f bj.<< ./Type /Fo

0020df0: 6e74 200d 2f53 7562 7479 7065 202f 5479 nt ./Subtype /Ty

0020e00: 7065 3020 0d2f 4e61 6d65 202f 4631 200d pe0 ./Name /F1 .

0020e10: 2f42 6173 6546 6f6e 7420 2f23 4241 2344 /BaseFont /#BA#D

0020e20: 4123 4343 2345 3520 0d2f 4465 7363 656e A#CC#E5 ./Descen

0020e30: 6461 6e74 466f 6e74 7320 5b20 3230 3720 dantFonts [ 207

0020e40: 3020 5220 5d20 0d2f 456e 636f 6469 6e67 0 R ] ./Encoding

0020e50: 202f 4742 4b2d 4555 432d 4820 0d3e 3e20 /GBK-EUC-H .>>

Type0复合字体:基本字体名 BADA CCE5 是 黑体,编码是 GBK-EUC-HCID子字体210 0 R (PDF对象ID)

0020e60: 0d65 6e64 6f62 6a0d 3230 3720 3020 6f62 .endobj.207 0 ob

0020e70: 6a0d 3c3c 200d 2f54 7970 6520 2f46 6f6e j.<< ./Type /Fon

0020e80: 7420 0d2f 5375 6274 7970 6520 2f43 4944 t ./Subtype /CID

CID Type2 字体:基本字体 黑体,WinCharSet 0x86,描述符208 0 R(PDF对象ID)

0020e90: 466f 6e74 5479 7065 3220 0d2f 4261 7365 FontType2 ./Base

0020ea0: 466f 6e74 202f 2342 4123 4441 2343 4323 Font /#BA#DA#CC#

0020eb0: 4535 200d 2f57 696e 4368 6172 5365 7420 E5 ./WinCharSet

0020ec0: 3133 3420 0d2f 466f 6e74 4465 7363 7269 134 ./FontDescri

0020ed0: 7074 6f72 2032 3038 2030 2052 200d 2f43 ptor 208 0 R ./C

0020ee0: 4944 5379 7374 656d 496e 666f 203c 3c20 IDSystemInfo <<

0020ef0: 2f52 6567 6973 7472 7920 284b 77b0 6789 /Registry (Kw.g.

0020f00: 292f 4f72 6465 7269 6e67 2028 4d51 ee29 )/Ordering (MQ.)

0020f10: 2f53 7570 706c 656d 656e 7420 3220 3e3e /Supplement 2 >>

0020f20: 200d 2f44 5720 3130 3030 200d 2f57 205b ./DW 1000 ./W [

0020f30: 2038 3134 2039 3037 2035 3030 2037 3731 814 907 500 771

0020f40: 3620 3737 3136 2035 3030 205d 200d 3e3e 6 7716 500 ] .>>

0020f50: 200d 656e 646f 626a 0d32 3038 2030 206f .endobj.208 0 o


问题求解

  1. 没有GBK-EUC-H对应的encoding

    1. 参数StandardEncoding创建一个
    2. 不能正常工作
  2. GBKEucHEncoding没有提供正确的getCharacter方法
    1. 编制一个自行分析byte值是否在GBK范围
    2. 不能正常工作
  3. 发现PDFBox的逻辑有问题 font.encode()是不可能返回null的,所以实际上不会执行双字节部份
  4. 发现PDFBox的问题在于,cmap/GBK-EUC-H parse之后居然没有doubleByteMap
  5. 确认认CMapParser没有处理begincidrange,只认识beginbfrange/char/space
    1. 加上begincidrange处理
    2. 不对,cidrange只影响从字体查找glyphy
  6. 回到encoding
    1. 修正PDFont.getCodeFromArray()
    2. GbkEucHEncoding.getCharacter()分析lead byte并转换bytes为单字符string
    3. 发现并patch 以解决encode()总是在lead byte时就返回一个单字节字符
    4. 测试通过

Holly(http://www.jsfsoft.com:8080/beyond-pebble/lee)认为可以有更为优雅的解决方案并做了一个patch,patch已经提交给pdfbox项目,在官方版本更新之前,你可以直接从这里获得这个patch(http://sourceforge.net/tracker/index.php?func=detail&aid=1640071&group_id=78314&atid=552834)

update log:
2007-1-20
published
2006-8-21
created
BLOG | English | 2005 2004 2003 2002-2001 2000 1999-1998

 
Web www.pinxue.net
沪ICP备06002693号