itext读写pdf的原理
之前介绍过使用ASM框架的一些基础api,里面会涉及.class的文件结构,字节码处理其实是修改.class文件的内容,关键内容在于对文件结构和字节码指令的了解
最近也阅读了些pdf相关的java框架,对其标准和原理做了些简单理解,于是以同样的方式,根据itext这个框架来解读分享下pdf的文件结构以及读写pdf的一些原理
首先用itext5.3.4生成一个pdf文件作为学习案例,itext采用事件驱动的方式来设计,创建一个pdf文件其实也很容易,只需要走5步即可,具体可以参见代码示例(创建document对象->定义PdfWriter->打开document->为document添加内容->关闭document)
public classHeaderFooter_ {
public static voidmain(String[] args) {
try {
//1.创建一个document
Document document = new Document();
//2.定义pdfWriter,指明文件输出流输出到一个文件
PdfWriter.getInstance(document,newFileOutputStream("D:\\test.pdf"));
//3.打开文档
document.open();
//字体
Font font = new Font();
font.setFamily("STSongStd-Light");
//颜色
font.setColor(BaseColor.BLUE);
//4.添加内容
Paragraph content = new Paragraph("xxx!",font);
document.add(content);
//添加段落
for(int i=0;i<100;i++){
document.add(new Paragraph("HelloWorld"+","+"Hello iText"+","+"HelloxDuan"));
}
//5.关闭
document.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
}
}
其实作为合格的码仔,学习使用api的速度那是非常快的,基本上很容易上手,基本一看就了解生成了有3页内容的pdf文件,所以使用api不是本次分析的目的,我们会更关注api的背后有什么东西。
好,现在我们来看下生成的test.pdf文件具体是以什么方式存放的,先写一段测试代码,以2进制方式读出pdf内容。
FileInputStream in = newFileInputStream(new File("D:\\test.pdf"));
int xx = in.read();
while(xx!=-1){
System.out.print((char)xx);
xx = in.read();
}
输出结果:
到此基本可以看出,pdf二进制内容大部分能直接读懂,这里要提下pdf文件是支持7位ASCⅡ码和二进制码这两种编码方式的.
从这个文件中,我们已经知道在pdf标准上将这个文件结构进行了分类,如下图,分为4部分:文件头、文件体、交叉引用表、文件尾
pdf文件内容的逻辑组织结构,也很好的反映了文件体中各间接对象间的等级层次关系。PDF的文档结构是一种树型结构,如图二所示。树的根节点也就是PDF文件的根对象,根节点下面有四个子树:页面树(Pages Tree)、书签树(Outline Tree)、线索树(ArticleThreads)和名字树(NamedDestination)。
下面就来一一解读pdf里的意义。
文件头:
第一行内容为“%PDF-1.4\r” -->第0-4位为固定值标识文件类型 %PDF或者%FDF等,第6-8为则是主次版本号
第二行的内容为:%226 227 207 211 \r,将这串2进制值转为16进制得到E2 E3 CF D3,这个是GBK编码“忏嫌”的中文,意思是容忍不满意,作者选择这个能够理解pdf的这种设计。另外文件头中的%还有另外一个含义,那就是注释,这是因为PDF其实是PostScript的缩略版,也是程序,可以自己编程来完成绘图、制作pdf;(注:PostScript是一种描述打印机如何在页面上绘图的语言,每个PostScript文件也可以称为一个矢量图片集,虽然它其实更像程序而不是图片)。
文件体
再下面,我们可以看到如下内容
4 0 obj
<</Parent3 0 R/Contents 2 0 R/Type/Page/Resources<</ProcSet [/PDF /Text /ImageB/ImageC /ImageI]/Font<</F1 1 0 R>>>>/MediaBox[0 0 595842]>>
endobj
6 0 obj
<</Parent3 0 R/Contents 5 0 R/Type/Page/Resources<</ProcSet [/PDF /Text /ImageB/ImageC /ImageI]/Font<</F1 1 0 R>>>>/MediaBox[0 0 595842]>>
endobj
8 0 obj
<</Parent3 0 R/Contents 7 0 R/Type/Page/Resources<</ProcSet [/PDF /Text /ImageB/ImageC /ImageI]/Font<</F1 1 0 R>>>>/MediaBox[0 0 595842]>>
endobj
3 0 obj
<</Type/Pages/Count3/Kids[4 0 R 6 0 R 8 0 R]>>
endobj
文件体里面由若干个的obj对象来组成。
第一个数字称为对象号,来唯一标识一个对象的,第二个是产生号,是用来表明它在被创建后的第几次修改,所有新创建的PDF文件的对象号应该都是0,即第一次被创建以后没有被修改过。上面的例子就标识了该对象的对象号是4,而且创建后没有被修改过。
包含在<<和>>之间的内容就是对象内容,最后以关键字endobj结束.
对象内容中主要为PostScript,在pdf易做图有60个页面描述指令,这60个页面描述指令描述了页面上的一系列的图形对象。这些图形对象大致可以分为四类,即路径对象(Path Object)、文本对象(Text Object)、图像对象(Image Object)和外部对象。
上面出现的这种代码(/Parent3 0 R)称为过程函数:过程函数是自定义新指令的方法,由一系列被“{”和“}”括起来的指令构成。例如求x的平方的过程为先复制一个x,再把两个x乘起来,即“{dupmul}”。于是该过程的定义指令为“/square {dup mul} def”。def是一个特殊的指令,可以把名称和数值或者过程函数结合起来并压入字典堆栈。基本格式为
/名称 取值 def
该指令用于定义变量和过程函数。当“取值”是一个过程函数,并且里面用到一些别的指令的时候,可以用
/名称 过程函数bind def
所以/Parent3 0 R表示意思是他的父路径是3 0 obj,从/Kids[40 R 6 0 R 8 0 R]中得知3 0 obj对象有40 obj、6 0 obj、8 0 obj 这3个叶子。即页面树与页面的关系。
其实过程函数只是做定义描述,并没与做什么其他事情。
补充:综合编程 , 其他综合 ,