使用IText生成PDF文件,并记录一些遇到的问题

2 篇文章 1 订阅

写在前面:

itext没有中文文档,我是没找到。但是itext官网提供了电子版的使用说明,特别详细。美中不足的是英文版的,自己也是使用翻译软件磕磕绊绊的读了一部分,所以记录的某些内容可能并不准确,或者自己理解有偏差。欢迎大家指正,万分感谢

生成PDF的流程

引入依赖

<dependency>
			<groupId>com.itextpdf</groupId>
			<artifactId>itextpdf</artifactId>
			<version>5.5.11</version>
</dependency>

创建pdf文件五步曲

	Document document = new Document();//1.创建document实例
	FileOutputStream out=new FileOutputStream(new File("D:/text.pdf"));
	PdfWriter writer = PdfWriter.getInstance(document, out);//2.创建PdfWriter 实例,并指定输出路径。
	document.open();//3. 打开 document实例,开始想document中添加内容
	//添加内容
	document.add(new Paragraph("test......."));//4.添加内容
	document.close();//5. 关闭
1.字体问题

itext默认的字体是不支持中文的,这样会导致只有某些西欧字符可以显示。如果需要支持中文,需要自己创建字体。

解决不支持中文显示,主要有以下两种解决方案:

一、使用系统环境下的字体
在window环境下查找字体
  1. 可以去fonts目录下查找。例如 :C:\WINDOWS\FONTS\SIMHEI.TTF,这个是黑体。
  2. 控制面板-外观个性化-字体。 可以查找对应字体,该方式比较直观,可以看到字体的名称。
    在这里插入图片描述
    选好字体,右键选择属性,切换到安全选项卡。就可以看到字体的路径。 例如选中楷体常规,右键就可以看到字体路径。
    在这里插入图片描述
自定义字体

window下环境不区分大小写,但是linux区分需要注意一下。

 String fontPath="C:/WINDOWS/FONTS/SIMHEI.TTF";
 BaseFont baseFont = BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
 Font font =new Font(baseFont,20f,Font.BLOD,BaseColor.BLACK);
 //创建了一个自定义字体格式,字体大小:20  样式:粗体  颜色:黑色

这样就可以在其他地方使用这个字体了。

/**
*@param name :字体的名称或者路径
*@param encoding: 使用的编码方式
*@param embedded : 字体是否嵌入到Pdf文件中。true:嵌入
*/
 public static BaseFont createFont(String name, String encoding, boolean embedded) throws DocumentException, IOException

关于字体嵌入参数:

  • BaseFont.NOT_EMBEDDED 字体不会嵌入到PDF文件中(没有保存),可能会导致不同电脑显示不正确。
  • BaseFont.EMBEDDED 使用的字体会嵌入(保存)到PDF文件中,这样文件会更大一些,这样PDF文件显示就是跨平台的。

注意:

Note that the BaseFont.EMBEDDED parameter will be ignored for the standard Type 1 fonts
iText will ignore the embedded parameter and always embed a subset of the font if you use the encoding IDENTITY_H or IDENTITY_V

  1. 如果创建BaseFont对象时,使用 IDENTITY_H or IDENTITY_V 编码类型,那么Itext会忽略 BaseFont.NOT_EMBEDDED参数并且总是使用该字体的子集(只会嵌入用到的字体)。

    我在创建BaseFont对象时指定的时非嵌入,可以看到 实际使用的是嵌入字符。

    BaseFont baseFont=BaseFont.createFont("pdf/MSYH.TTF", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED)
    

    在这里插入图片描述

  2. itext 中存在14种标准的Type1 字体,对于标准的Type 1字体,将忽略嵌入式参数 BaseFont.EMBEDDED。这是因为iText没有附带这些字体的PostScript字体二进制(PFB)文件。

    These are the Adobe Font Metrics (AFM) files for the 14 standard Type 1 fonts: 4 Helvetica fonts (normal, bold, oblique, and bold-oblique), 4 TimesRoman fonts (normal, bold, italic, and bold-italic), 4 Courier fonts (normal, bold,
    oblique, and bold-oblique), Symbol, and Zapf Dingbats

    // 使用 Times New Roman 字体
    BaseFont bf = BaseFont.createFont(BaseFont.TIMES_ITALIC, BaseFont.WINANSI, BaseFont.EMBEDDED);
    

    该字体不会被嵌入。
    在这里插入图片描述

  3. TrueType collection(ttc格式字体)中的字体由索引寻址。
    比如:微软雅黑(msyh.ttc),创建BaseFont 实例时还需要传入",数字"。数字表示字体索引。

    // 使用
     BaseFont baseFont = BaseFont.createFont("D:\\fonts\\msyh.ttc,0", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
    

    如果该方式还是无法显示中文,可以尝试下载微软雅黑字体ttf格式文件。

  4. 对于嵌入字体 使用setSub(false),可以用于嵌入所有的文字。

二、引入字体集jar

iTextAsian.jar 里面预制了各种字体,用于中日韩等文字的显示问题。

引入依赖

<dependency>
		   <groupId>com.itextpdf</groupId>
		    <artifactId>itext-asian</artifactId>
		    <version>5.2.0</version>
</dependency>

在这里插入图片描述
对于中文显示 iTextAsian.jar 提供了两种字体,每种字体都对应两种编码(字符映射)。

stsong-light-unigb-ucs2-h字体就是华文宋体的英文写法,该字体横细竖粗。

BaseFont baseFont = BaseFont.createFont( "STSongStd-Light" ,"UniGB-UCS2-H",BaseFont.NOT_EMBEDDED);
Font font = new Font(baseFont, 12,Font.NORMAL);

创建字体时可以指定 一些字体样式 : Bold(粗体), Italic(斜体), or BoldItalic(粗斜体)。

BaseFont baseFont = BaseFont.createFont( "STSongStd-Light,Italic" ,"UniGB-UCS2-H",BaseFont.NOT_EMBEDDED);
Font font = new Font(baseFont, 12,Font.NORMAL);

在这里插入图片描述
可以看到STSongStd-Light 类型是typ1类型。当然 PDF文档视图中展示的字体类型和字体文件关联的字体类型有差别。
在这里插入图片描述
注意:

Note that whatever value you pass for the ‘embedded’ parameter with the method BaseFont.createFont, the font WILL NOT BE embedded.

使用这种方式创建BaseFont 对象 会忽略 Font.EMBEDDED 参数,字体总是非嵌入。 这是因为在 iTextAsian.jar 中属性文件和CMAP文件不包含字形描述。

使用上面创建pdf创建的五部曲,就可以添加元素显示中文了。

		
			OutputStream out=null;
			Document doc=new Document();
			try {
				//创建各种类型字体的基类。
				BaseFont baseFont = BaseFont.createFont( "STSongStd-Light" ,"UniGB-UCS2-H",BaseFont.NOT_EMBEDDED);
				// BaseFont baseFont = BaseFont.createFont("D:\\fonts\\msyh.ttc,0", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
				
				//根据创建的中文基类,就可以创建各种样式的字体。
				//标准,字体大小12 
				Font font = new Font(baseFont, 12,Font.NORMAL);
				//粗体,字体大小12 
				Font blodFont= new Font(baseFont, 12,Font.NORMAL);
				out=new FileOutputStream("D:/font_test.pdf");
				PdfWriter writer=PdfWriter.getInstance(doc, out);
				doc.open();
				doc.add(new Paragraph("测试",font));
				doc.close();
			}catch (Exception e) {
				e.printStackTrace();
				if(out!=null) {
					try {
						out.close();
					} catch (IOException e1) {
						e1.printStackTrace();
					}
				}
				
			}

在这里插入图片描述

三、创建字体,并携带其他字体属性

如同css 一样, 字体还可以设置 粗细、样式、大小、样式、颜色

 font :  font-style font-weight font-size font-family;

itext 提供了如下构造方法,方便我们实现自定义的字体

public Font(final BaseFont bf)

public Font(final BaseFont bf, final float size)

public Font(final BaseFont bf, final float size, final int style)

public Font(final BaseFont bf, final float size, final int style, final BaseColor color)
字体样式

参数style取值, 如下

// Font 提供了如下,静态变量用于设置 style

//正常
public static final int NORMAL = 0;

// 粗体
public static final int BOLD = 1;
// 倾斜
public static final int ITALIC = 2;
// 下划线
public static final int UNDERLINE = 4;

//删除线
public static final int STRIKETHRU = 8;

// 加粗+斜体
public static final int BOLDITALIC = BOLD | ITALIC;

当然如同BOLDITALIC 一样, 我们可以组合多个样式

BaseFont baseFont = BaseFont.createFont(fontPath,BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

// 加粗, 斜体,删除线
Font font = new Font(baseFont, 20f, Font.BOLD|Font.ITALIC | Font.STRIKETHRU);

// 正常字体
Font normalFont = new Font(baseFont, 20f);

![在这里插入图片描述](https://img-blog.csdnimg.cn/7d09a9c3e7b743eea2cb5e5fd6a90a9e.png

小结
  • iTextAsian 中 STSongStd-Light 字体就是window环境下的华文宋体,所以即便字体不会被嵌入,但是还是可以正常显示。
  • 创建 BaseFont 对象使用 编码方式 BaseFont.IDENTITY_HBaseFont.IDENTITY_V 会忽略 嵌入参数,并总是嵌入字体 。
  • 建议使用嵌入方式,也就是第一种方式。这样不用引用额外的包,还不用担心 乱码的问题。
  • linux下区分大小写,且路径以 / 结尾。
关于字体的一些扩展
字体文件和扩展名

Type 1 PostScript最初是Adobe拥有的专有规范,之后Apple公司制定了TrueType 字体规范,为了竞争Type 1 规范被发布。允许第三方厂商基于Type 1 规范创建(Type 1 )字体。

早期使用 Type 1 PostScript 字体需要比较高的版权费用,所以苹果制定了TrueType字体规范之后,微软也加入TrueType阵营。之后苹果和微软分别对TrueType 增加了一些扩展。这就导致Windows的TrueType字体不一定适用于Mac。

微软开发了基于Apple的TrueType字体格式的扩展字体,后来Adobe也加入了开发,TrueType Open 兼容了PostScript Type 1 成为了一种新得字体类型OpenType,该类型兼容 TrueType和Type1 PostScript(Type2)。

字体类型扩展名说明
Type 1字体文件.afm, .pfm,.pfbType 1最初是Adobe拥有的专有规范
TrueType 字体文件.ttf基于苹果开发的规范的字体
OpenType 字体文件.otf, .ttf,.ttc一种基于Unicode字符集跨平台字体文件格式

OpenType

  • ttf: 基于TrueType Outline (TrueType 轮廓?)的字体的使用文件后缀名.ttf(OpenType TT)
  • ttc :在单个文件中包含多个 TrueType 类型字体,这也是名字的由来TrueType Collection。
  • otf : 字体文件包含 PostScript Type 1的使用 .otf后缀( OpenType PS)

OpenType具有以下优点:

  • 更大的字形限制(64k)
  • 跨平台支持(Win和Mac)
  • 支持PostScript Type 1或TrueType轮廓
  • 支持高级印刷功能

正因为 ttc后缀的字体时由多个TrueType字体组装而成的,所以创建 BaseFont字体时需要指定索引。
这样使用 ttf后缀的文件即有可能是TrueType字体也有可能是使用 TrueType Outline的OpenType 字体。Itext会帮助我们去区分这两种字体的差异,所以对我们使用而言是没有什么区别的。

目前Windows环境下的字体大多都是OpenType TT(OpenType with TrueType Outline)类型。

好吧,查阅了很多资料以后发现真得是越来越乱了,可以参考window OpenType 技术规范

PDF 视图中的字体类型

上面介绍的的是不同字体类型使用不同的文件来组织。
下面介绍的是可以在PDF文档中显示的字体子类型

子类型描述
/Type1使用Type 1字体技术定义字形形状的字体
/Type3一种用PDF图形操作符流定义字形的字体。 形如:△
/TrueType基于TrueType字体格式的字体
/Type0复合字体—由从CIDFont派生的字形组成的字体
/CIDFontType0一种CIDFont, 它的字形描述基于紧凑字体格式(CFF)
/CIDFontType2一种CIDFont,它的字形描述基于TrueType字体技术

字形是指 字体在页面的展示形状,例如:草书,正楷它们的字形就是千差万别的。

前三个子类型使用简单字体,Type0 复合字体。

简单字体的字形使用个一个字节表示(字符集范围0~255)。复合字体(字符集索引范围0 ~ 65,535)使用类型/Type0 表示。

CIDFont 这种字体是Adobe公司为大字符集语言设计的 。
CID代表字符标识符。字符标识符用作字符集合的索引,以访问(索引)字体中的符号 (字形)。在处理具有大量字符集的语言时,比如汉语、日语和韩语 ,具有很大的优势。

CIDFont 有两种类型:

  • Type 0 CIDFonts(CIDFontType0):这个就是 OpenType 中的Typ1字体类型(基于紧凑的字体格式)。如果想嵌入这种字体,通常需要购买一个支持这种格式的.otf字体文件。 在PDF视图中我么看到它实际显示的类型为Type 1 (CID)
  • Type 2 CIDFonts(CIDFontType2):它包含的字形基于TrueType类型。它对应了 带有TrueType 轮廓的OpenType字体( OpenType with TrueType outlines )(.ttf) 或 TrueType 集合(.ttc).

Unicode 编码和 字体中CID之间的关联关系在CMap中指定(一种映射关系)。CMAP 常用的编码有两种: IDENTITY_H、IDENTITY_V。

CIDFonts are collections of glyphs that can’t be used directly. They can only be used as a component of a Type 0 font

关于乱码问题

PDF内容显示正常,但是复制PDF文本到本地时乱码。
出现这种情况应该满足下面的条件:

  • 使用嵌入的字体(PDF文件可以正常预览)。如果存在非嵌入字体那也是可以被Adobe 识别的。
  • 字体集(字体)本地系统中不存在(复制到本地记事本是乱码)。

之前有同学问过我这个问题,看到这个问题的时候 我当时并没有给出具体的解决方案,只是隐约觉得字体集有问题,经过证实也确实是之前这份重要的PDF文件是在苹果上生成的。但是因为那份PDF文件嵌入的字体特别多(当时没有观察字体的类型),所以最后网上找的ORC 识别软件另存了一份可用的PDF文件。
作为一个程序员正确的解决方法:
在这里插入图片描述
查找为嵌入字符集的字体,本地不存在时安装字体到本地系统(C:\windows\fonts)。

PDF内容显示乱码问题:

  • BaseFont 定义的字体不支持要显示的内容
  • 字体未嵌入,有些系统中不存在这个字体。
  • 待补充。。。。。
2.文本内容

下面涉及的代码很多都是只写了一部分,为了代码更简洁,关于创建PDF的五个基本流程和 字体的创建可能就没有体现。这一份代码可以参照 字体这一章节。

还要就是下面的代码有一部分就是手打的所以可能会有错误。

1)Chunk(块)
  1. Chunk元素是可以添加到Document中的最小文本元素。
  2. Chunk对象中包含一个StringBuffer对象,来表示Chunk的文本内容
  3. StringBuffer 中包含的文本具有相同的字体(颜色,大小,样式)
  4. Chunk 元素对多行文本不敏感(默认行间距为0,这样多行文本会重叠)。通过代码 PdfWriter.getInstance(doc, out).setInitialLeading(60f);修改这一行为。
  5. 通常不直接将Chunk元素添加到Document对象中
  6. 同时还可以为块元素指定下划线,背景色,上标等等。同时还可以为块元素指定下划线,背景色等等。

常用的方法或属性

public static final Chunk NEWLINE = new Chunk("\n"); //新增一行

/**
	 * Sets an horizontal line that can be an underline or a strikethrough.
	 * Actually, the line can be anywhere vertically and has always the <CODE>
	 * Chunk</CODE> width. Multiple call to this method will produce multiple
	 * lines.
	 *
	 * @param thickness
	 *            下划线的厚度(线的宽度)
	 * @param yPosition
	 *            相对于中心线(baseline)的距离
	 * @return this <CODE>Chunk</CODE>
	 */
public Chunk setUnderline(final float thickness, final float yPosition);//虽说是设置下划线但是只要调整 yPosition的值,就可以设置删除线

public Chunk setBackground(final BaseColor color);//设置背景色

public String getContent();//获取Chunk文本的内容

/**
	 * Sets the text displacement relative to the baseline. Positive values rise
	 * the text, negative values lower the text.
	 * 该方法可以用来实现上标和下标
	 * @param rise
	 *            设置该chunk元素显示的位置 rise>0 设置上标,rise<0设置下标
	 * @return this <CODE>Chunk</CODE>
	 */
public Chunk setTextRise(final float rise); //设置上下标,

Chunk_Demo(包含下划线,上标,行距)


	@RequestMapping("/testPdf")
	public void testPdf(HttpServletRequest request,HttpServletResponse response) {
		
		OutputStream out=null;
		Document doc=new Document();
		try {
			out= response.getOutputStream();
			PdfWriter writer=PdfWriter.getInstance(doc, out);
			writer.setInitialLeading(60f); //设置行距
			doc.open();
			
			Chunk chunk=new  Chunk("N");
			chunk.setUnderline(0.2f, -3f);//下划线
			doc.add(chunk);
			Chunk id = new Chunk("2"); //上标
			id.setTextRise(6);//正数
			doc.add(id);
			doc.add(new Paragraph("\n"));
			
			doc.add(new Chunk("N"));
			id = new Chunk("2");//下标
			id.setTextRise(-6);//负数
			doc.add(id);
			
			doc.close();
		}catch (Exception e) {
			e.printStackTrace();
		}finally {
			if(out!=null) {
				try {
					out.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}	

在这里插入图片描述
注意如果去掉 代码 writer.setInitialLeading(60f);,会发现元素是重合的。所以说Chunk元素对多行元素不敏感。效果如下
在这里插入图片描述

Chunk类中有几个常用的常量:

//另起一行
public static final Chunk NEWLINE = new Chunk("\n");
// 另起一页
public static final String NEWPAGE = "NEWPAGE";

之前我总以为 Chunk.NEWLINE 是一个空白行。直到我看到源码,这个在Phrase中也会提到。

2)Phrase (持有行间距的多个Chunk)
  1. Phrase元素内置ArrayList对象用来容纳多个Chunk元素
  2. 有默认的间距,所以可以不用设置间距
  3. 每个Chunk元素都可以有自己的特定的样式
  4. 可以添加多个Paragraph元素或者Chunk元素。
行间距 (leading 和 multipliedLeading)

leading: 是固定值。
multipliedLeading : 默认值为0。这个这是的是相对值,相对于字体大小。当设置了multipliedLeading 时,字体大小越大则间距越大。这个
最终行间距 :totalLeading = leading+fontSize*multipliedLeading ;

Phrase_Demo

	Phrase p=new Phrase();
	p.add(new Chunk("正常",normalFont));
	//换行
	p.add(Chunk.NEWLINE);
	p.add(new Chunk("粗体",blodFont));
	doc.add(p);

显示效果如下:
在这里插入图片描述
可以看到Phrase 具有默认的行间距,两个Chunk元素没有重叠。从这里也可以看出 Phrase .add(Chunk.NEWLINE); 是指另起一行。

3)Paragraph(段落,自带换行的Phrase)
  1. Phrase的子类。
  2. 通常来说需要添加 Phrase元素或者Chunk元素。 如果不能正确的添加元素,会导致一些设置失效。

设置失效的例子(Paragraph 对象添加多个Paragraph 对象):

			Phrase p=new Phrase();
			p.add(new Chunk("正常",normalFont));
			p.add(Chunk.NEWLINE);
			p.add(new Chunk("粗体",blodFont));
			
			Paragraph paragraph=new Paragraph();
			paragraph.add(p);
			paragraph.add(new Paragraph("xxxx"));//可以自动换行,但是无法设置间距
			paragraph.add(new Paragraph("xxxx"));
			paragraph.add(new Paragraph("xxxx"));
			paragraph.add(new Paragraph("xxxx"));
			paragraph.setLeading(40f);
			paragraph.setSpacingBefore(10f);
			doc.add(paragraph);

在这里插入图片描述
上面可以看到 虽然添加的每个Paragraph元素都会另起一行,但是设置的行间距貌似没有起什么作用。

	Phrase p=new Phrase();
	p.add(new Chunk("正常",normalFont));
	p.add(Chunk.NEWLINE);
	p.add(new Chunk("粗体",blodFont));
	Paragraph paragraph=new Paragraph();
	paragraph.add(p);
	paragraph.add(new Phrase("xxxx\n"));//new Chunk("xxxx\n")  需要手动添加回车进行换行
	paragraph.add(new Phrase("xxxx\n"));//new Chunk("xxxx\n")  需要手动添加回车进行换行
	paragraph.add(new Phrase("xxxx"));
	paragraph.add(new Phrase("xxxx"));
	paragraph.add(new Paragraph("hahahha"));
	paragraph.setLeading(40f);

在这里插入图片描述
从上面也可以看出,phrase 、Chunk不会换行直到一行显示不开。

经过测试发现:

  1. Paragraph.add(Phrase| Chunk) 行间距属性有效
  2. Phrase.add(Paragraph | Chunk | Phrase) 行间距属性有效,此时需要手动添加换行符进行手动换行。毕竟Phrase是不支持换行的(如果该行内容未满)。

对于Paragrap还可以设置内容的对齐方式。

public void setAlignment(int alignment);

alignment 值都定义在Element元素中,因为Paragrap是Element的子类,所以它可以直接访问这些静态常量。

对齐方式描述
ALIGN_RIGHT右对齐
ALIGN_LEFT左对齐
ALIGN_CENTER居中对齐
ALIGN_JUSTIFIED两边对齐

在这里插入图片描述

3.表格相关

和表格相关的类主要有两个:

  • PdfPTable : 用于创建表格
  • PdfPCell: 用于创建单元格
PdfPTable
添加单元格

表格使用 addCell方法添加单元格。
在这里插入图片描述
可以看到单元格内容可以是图片、字符串(中文不显示)、Phrase对象、PdfPCell对象、PdfPTable对象。
在不同场景下我们使用不同的方法。

  • addCell(String) 添加数字、符号或者英文字符。该方式无法设置字体大小,颜色等
  • addCell(Phrase): 添加具有自定义字体样式的文本(字体大小,颜色,样式,字体)。
  • addCell(PdfPcell): 设置具有属性的表格。(跨行,跨列,边框,背景色等)
  • addCell(PdfPTable): 表格嵌套
a.表格的列数问题

创建PdfPTable实例,必须传入列数。 因为在itext中使用流式显示方式,当某一行指定列数显示完成,就会自动转向下一行。

1、需要注意的是,这种流式元素添加,会使某些设置失效。(比如单元格跨列)

		PdfPTable table=new PdfPTable(2);
		//添加单元格
		table.addCell(new PdfPCell(new Phrase("cell0")));
		PdfPCell cell=new PdfPCell(new Phrase("cell1"));
		cell.setRowspan(2);//跨两列
		table.addCell(cell);

即便设置了 cell1 跨两列,实际显示结果是没有跨列。
在这里插入图片描述
2、 某一行的单元格必须填充满,否则有些意想不到的结果。

PdfPTable table=new PdfPTable (2);//创建一个表格,它有两列
table.addCell(new PdfPCell(new Phrase("cell0")));
table.addCell(new PdfPCell(new Phrase("cell1")));
table.addCell(new PdfPCell(new Phrase("cell2")));//缺少一列,最后一行不显示

cell2 不显示
在这里插入图片描述
所以需要额外注意的是,如果某一个单元格没有内容,也必须创建并添加单元格。如果某一行的所有列并没有填充完成,就添加到document(等容器中)则该表格的最后一行不显示。 因为这种流式添加数据,所以有某个单元格没有被添加,最后肯定表现在最后一行上。
所以遇到表格行数显示不正确的问题,需要检查是否每一行中的单元格数量于列数相同。

3、还可以使用一种简单的方式添加单元格:使用completeRow()方法。需要注意compleRow方法只针对于当前行,并不是所有行。

PdfPTable table=new PdfPTable(2);//创建一个表格,它有两列
table.addCell(new PdfPCell(new Phrase("cell0")));
table.completeRow();
table.addCell(new PdfPCell(new Phrase("cell1")));
table.addCell(new PdfPCell(new Phrase("cell2")));

在这里插入图片描述
注释掉 table.completeRow(); 方法,会发现只显示 cell0、cell1

该方法也不过是向该行追加默认的单元格,追加的数量取决于该行缺少的列数。

	protected PdfPCell defaultCell = new PdfPCell((Phrase) null);
    public void completeRow() {
        while (!rowCompleted) {
            addCell(defaultCell);
        }
    }

小结
所以表格添加需要注意一些细节,如果注意不到可能会有出乎预料的结果。总结一下

  1. 流式添加元素。如果了解过swing的话(流式布局),可能会有很好的理解。
  2. 必须设置列数
  3. 每一行添加的单元格数量必须和列数一致,否则可能出现缺少行数的情况(completeRow() 辅助方法)
  4. completeRow在某些情况下也是不生效的。比如某一个单元格跨行,那么调用completeRow 方法是不生效的。
b.表格的宽度问题 | 单元格宽度

每个PdfPTable保存两个宽度值:

  • widthPercentage: 相对宽度,相较于可用宽度的百分比。
  • totalWidth: 绝对宽度,设置固定的宽度。

PdfPTable 使用哪个宽度取决于 lockedWidth 的值。

  /**
   *lockedWidth=true  那么使用 totalWidth,否则使用widthPercentage
   */
 public void setLockedWidth(final boolean lockedWidth) {
        this.lockedWidth = lockedWidth;
    }
    //默认为false
  private boolean lockedWidth = false;
  1. widthPercentage
    默认情况下新增的table的宽度占父容器宽度的80%而不是100%。所以添加的表格左右两边总有明显的留白。
    我上面有很多截图都有很宽的留白,就是因为我使用的是table的默认宽度。

    table.setWidthPercentage(100);//设置表格宽度为父容器宽度的100%
    

    一般表格嵌套时会经常使用该方法,内嵌表格完全填充父容器(外表格的单元格)。

    		PdfPTable outTable=new PdfPTable(2);// 外表格
    		PdfPCell cell=new PdfPCell();
    		PdfPTable innerTable=new PdfPTable(2);//内表格
    		innerTable.setWidthPercentage(100);// 宽度占比100%,使内表格充满外表格的单元格
    
    		PdfPCell cell1=new PdfPCell(new Phrase("cell0"));
    		cell1.setBorderWidthTop(0);//边框宽度设置为0 ,即不显示边框
    		cell1.setBorderWidthLeft(0);
    		cell1.setBorderWidthBottom(0);
    		innerTable.addCell(cell1);//添加 单元格
    		cell1=new  PdfPCell(new Phrase("cell1"));
    		cell1.setBorderWidthTop(0);
    		cell1.setBorderWidthRight(0);
    		cell1.setBorderWidthBottom(0);
    		cell1.setBorderWidth(0);
    		innerTable.addCell(cell1);//添加单元格
    
    
    		cell.setPadding(0);//设置内边距为0
    		cell.addElement(innerTable);//添加内嵌表格
    
    		outTable.addCell(cell);
    		outTable.addCell(new Phrase("xxxx"));
    

    在这里插入图片描述

    我们会发现内嵌表格的宽度和外表格单元格所占宽度一样,这是因为PdfPTable默认配置单元格宽度为 1:1。
    我们可以通过PDFPTable.setWidths 方法修改这一默认行为。

    public void setWidths(final float relativeWidths[])
    public void setWidths(final int relativeWidths[])
    

    通过上面的方法就可以设置单元格宽度的比例关系。

     PdfPTable table=new PdfPTable(2);
     //设置单元格宽度比例 1:3
     table.setWidths(new int[]{1,3});
     table.addCell(new Phrase("cell1",font));
     table.addCell(new Phrase("cell2",font));
     table.addCell(new Phrase("cell3",font));
     table.addCell(new Phrase("cell4",font));
    
    

    在这里插入图片描述

  2. totalWidth
    如果要使用该宽度首先修改PdfPTable属性值 lockedWidth。

    	PdfPTable table = new PdfPTable(2);
    	table.setTotalWidth(280);
    	table.setLockedWidth(true);
    	table.setWidths(new float[]{2, 1});
    	table.addCell(new Phrase("cell1",font));
    	table.addCell(new Phrase("cell2",font));
    	table.addCell(new Phrase("cell3",font));
    	table.addCell(new Phrase("cell4",font));
    

    在这里插入图片描述

    Note that you always have to set the total width if you intend to add the PdfPTable at an absolute position

小结:

  1. 默认情况下新增的table的宽度占可用宽度(父容器宽度)的80%而不是100%。setWidthPercentage 方法可以修改表格的宽度占比
  2. 单元格的默认宽度为表格(可用)宽度除以列数等分。setWidths方法可以修改单元格的宽度占比
  3. 表格默认使用 setWidthPercentage 来决定宽度。可以通过方法setLockedWidth 修改这一行为。
  4. PdfPTable无法根据内容动态调整单元格宽度。
c. 设置表格前后间距

表格可以设置前后间距

public void setSpacingAfter(final float spacing);
public void setSpacingBefore(final float spacing)
	PdfPTable table = new PdfPTable(2);
	table.setTotalWidth(288);
	table.setLockedWidth(true);
	table.setWidths(new float[]{2, 1});
	//添加单元格
	table.addCell(new Phrase("cell1",font));
	table.addCell(new Phrase("cell2",font));
	table.addCell(new Phrase("cell3",font));
	table.addCell(new Phrase("cell4",font));
	table.setSpacingBefore(50);
	table.setSpacingAfter(20);

在这里插入图片描述

d.表格对齐方式

单元格可以设置内容对齐,表格也可以设置水平对齐方式。

    PdfPTable table = new PdfPTable(2);
    table.setTotalWidth(288);
    table.setLockedWidth(true);
    table.setWidths(new float[]{2, 1});
    //添加单元格
    table.addCell(new Phrase("cell1",font));
    table.addCell(new Phrase("cell2",font));
    table.addCell(new Phrase("cell3",font));
    table.addCell(new Phrase("cell4",font));
    table.setHorizontalAlignment(PdfPTable.ALIGN_RIGHT);

在这里插入图片描述
这个对齐的常量值定义在Element元素中,因为PdfPTable 是Element 的子类,所以PdfPTable 可以直接访问这些常量值。

注意:

  • 只有表格设置的宽度少于可用宽度设置才有意义。
  • 表格只可以设置水平对齐。可用值:
    • Element.ALIGN_LEFT
    • Element.ALIGN_CENTER
    • Element.ALIGN_RIGHT
PdfPCell

单元格的
PdfPCell 的构造函数可以以Image、Phrase为参数。

a. 单元格内容对齐

我们可以通过以下方法设置单元格内容的对齐方式

//控制垂直对齐方式
public void setVerticalAlignment(int verticalAlignment);
//控制水平对齐方式
public void setHorizontalAlignment(int horizontalAlignment)

对齐的参数常量存放在Element元素中。

对齐方式描述
ALIGN_RIGHT水平对齐,右对齐
ALIGN_LEFT水平对齐,左对齐
ALIGN_CENTER水平对齐,居中对齐
ALIGN_TOP垂直对齐,垂直居上
ALIGN_MIDDLE垂直对齐,垂直居中
ALIGN_BOTTOM垂直对齐,垂直居下

很多场景下我们都是设置单元格内容水平垂直居中显示。

//水平居中
 cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
//垂直居中
 cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
b.单元格合并

在这里插入图片描述
在很多场景下我们需要进行单元格合并如同Excel一样。这时候需要借助一下方法:

// 跨行
public void setRowspan(int rowspan);
//跨列
public void setColspan(int colspan);

在表格列数问题这一小节中提到过 如果单元格添加不合理会导致某些设置失效。

	PdfPTable table = new PdfPTable(2);
	table.addCell(new Phrase("cell1",font));
	table.addCell(new Phrase("cell2",font));
	PdfPCell rowSpanCell = new PdfPCell(new Phrase("跨行",font));
	//跨两行
	rowSpanCell.setRowspan(2);
	table.addCell(rowSpanCell);
	//跨两列
	PdfPCell columnSpan = new PdfPCell(new Phrase("跨列",font));
	columnSpan.setColspan(2);
	table.addCell(columnSpan);

在这里插入图片描述

//包含两列的表格
 PdfPTable table = new PdfPTable(2);
 table.addCell(new Phrase("cell1",font));
  table.addCell(new Phrase("cell2",font));
  
  //跨两行单元格
  PdfPCell rowSpanCell = new PdfPCell(new Phrase("跨行",font));
  rowSpanCell.setRowspan(2);
  //水平居中
  rowSpanCell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
  //垂直居中
  rowSpanCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
  table.addCell(rowSpanCell);

  table.addCell(new Phrase("占位。。。",font));
  table.addCell(new Phrase("占位。。。",font));

  //跨两列单元格
  PdfPCell columnSpan = new PdfPCell(new Phrase("跨列",font));
  //水平居中
  columnSpan.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
  columnSpan.setColspan(2);
  table.addCell(columnSpan);

在这里插入图片描述
如果将 占位的单元格替换为 completeRow,就会发现对于跨行的单元格 在它所在行上使用completeRow方法无效。

//            table.addCell(new Phrase("占位。。。",font));
//            table.addCell(new Phrase("占位。。。",font));
            table.completeRow();
            table.completeRow();

在这里插入图片描述

c. 设置单元格 - 行间距

对于Phrase而言提供了setLeading()和setMultipliedLeading()方法设置行距。但是该方法设置的行距对PdfPCell这种容器中是不起作用的。需要使用PdfPcell#setLeading方法。
在这里插入图片描述
效果如下:
在这里插入图片描述
因为构造函数的方式phrase属性会丢失,设置无效。

  public PdfPCell(Phrase phrase) {
        super(0, 0, 0, 0);
        borderWidth = 0.5f;
        border = BOX;
        column.addText(this.phrase = phrase);
        column.setLeading(0, 1);//固定
    }

当然可以是使用另一种方式,不过更麻烦。使用 PdfPCell#addElement方法 添加元素。

	PdfPTable table=new PdfPTable(3);
		Phrase p=new Phrase("1111111111111\n22222222222222\n33333333\n");
		p.setLeading(10f, 1.4f);
		PdfPCell cell=new PdfPCell();
		//不是作为构造函数传入
		cell.addElement(p);
		table.addCell(cell);
		table.completeRow();
		doc.add(table);
d. 设置单元格 - 行高

Itext 提供了一下方法来设置行高

//设置单元格固定高度
setFixedHeight(25f);
//设置单元格最小高度。
// 文本如果可以适应此高度,则单元格高度就为设置的高度。否则高度值会增加。
setMinimumHeight(25f);

通常情况下,我们不会设置单元格高度,此时单元格高度自适应单元格的内容。

	//中文字体
 	BaseFont baseFont = BaseFont.createFont("pdf/font/MSYH.TTF", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
    Font font = new Font(baseFont, 20, Font.NORMAL);

	PdfPTable table=new PdfPTable(3);
	//高度不够大
	PdfPCell cell=new PdfPCell(new Phrase("你好",font));
	cell.setFixedHeight(25f);
	table.addCell(cell);
	cell=new PdfPCell(new Phrase("Hello world...............",font));
	cell.setFixedHeight(25f);
	table.addCell(cell);
	table.completeRow();
	
	//高度自适应
	cell=new PdfPCell(new Phrase("你好",font));
	table.addCell(cell);
	cell=new PdfPCell(new Phrase("Hello world...............",font));
	table.addCell(cell);
	table.completeRow();
	
	//高度过小
	cell=new PdfPCell(new Phrase("你好",font));
	cell.setFixedHeight(20f);
	table.addCell(cell);
	cell=new PdfPCell(new Phrase("Hello world...............",font));
	cell.setFixedHeight(20f);
	table.addCell(cell);
	
	table.completeRow();

在这里插入图片描述

不推荐设置 高度为固定值,除非你可以保证内容可以完全在单元格中显示。

注意:

  • 实践表明,如果要设置固定高度,需要为该行的每一个单元格都设置高度。否则结果可能与预期有差异。

  • 这是因为我们设置的是行高,所以如果该行 各个单元格设置的高度不统一,它会取最大值。
    在这里插入图片描述

     PdfPTable table=new PdfPTable(3);
    PdfPCell cell=new PdfPCell(new Phrase("你好",font));
     cell.setFixedHeight(10f);
     table.addCell(cell);
     cell=new PdfPCell(new Phrase("Hello world...............",font));
     cell.setFixedHeight(25f);
     table.addCell(cell);
     table.completeRow();
    
     cell=new PdfPCell(new Phrase("你好",font));
     cell.setFixedHeight(10f);
     table.addCell(cell);
     cell=new PdfPCell(new Phrase("Hello world...............",font));
     table.addCell(cell);
     table.completeRow();
    
e. 设置单元格 - 其他属性
  1. 设置单元格 宽度
    之前在 介绍 PdfPTable 的时候已经介绍过了。默认 单元格宽度是1:1 等分 表格的总宽度。单元格无法直接设置单元格的宽度, 可以通过setWidths 方法设置宽度占比。

     table=new PdfPTable(3);
     table.setWidths(new int[]{1,2,3});
    
  2. 设置内边距
    setPadding(),可以用来设置内边距.

    	setBorderPaddingTop();//上边框
    	setBorderPaddingBottom();//下边框
    	setBorderPaddingLeft();//左边框
    	setBorderPaddingRight();//右边框
    
  3. 设置边框宽度
    setBorderWidth,设置单元格边框宽度。如果需要设置某一个边的边框。

    	setBorderWidthTop();//上边框
    	setBorderWidthBottom();//下边框
    	setBorderWidthLeft();//左边框
    	setBorderWidthRight();//右边框
    
  4. 设置边框颜色
    setBorderColor(),设置边框颜色

    		setBorderColorTop();//上边框
    		setBorderColorBottom();//下边框
    		setBorderColorLeft();//左边框
    		setBorderColorRight();//右边框
    
4.目录

实现目录主要用到两个类 Chapter 和Section。其中Chapter是Section的子类,

Paragraph prag = new Paragraph("第一章", font);
Chapter chapter2 = new Chapter(title2, 1);
Section section =chapter.addSection(new Paragraph("1. 第一节"),font);
Section section =chapter.addSection(new Paragraph("1. 1 第1小节"),font);
Chapter

重要的概念:

  1. depth: 章节的深度,默认情况深度为1.
  2. title : 章节的标题
  3. number。章节的序号,只能为数字
  4. outline:书签,就是PDF左侧的大纲,可以快速定位

Chapter中有很多默认的行为:

  • 每个Chapter 默认是新起一页,可以通过setTriggerNewPage(false)修改这一默认行为

  • 默认情况下创建的chapter标题后面会追加逗号( “.”)。 可以通过setNumberStyle(NUMBERSTYLE_DOTTED_WITHOUT_FINAL_DOT)更改这一行为

  • 默认添加的所有子章节都会在左侧的书签中显示,可以通过setBookmarkOpen(false)修改这一行为。可以控制子标签是否显示。

  • 添加Chapter时,默认会创建书签。

  • 创建Chapter时必须传入 章节序号,而该序号也会显示在标题的前面。chapter.setNumberDepth(0);该方法可以控制标题不显示序号

			//创建Document,PdfWriter实例,创建字体
			//.....................
			Chapter firstChapter=new Chapter(2);
			firstChapter.setTitle(new Paragraph("章节一",font));
			firstChapter.add(new Phrase("first chapter depth:"+firstChapter.getDepth()));
			Section section = firstChapter.addSection("second1");//子章节
			section.setIndentationLeft(20f);
			section.add(new Phrase("second1   depth:"+section.getDepth()));
			section.addSection("second1.1").setIndentationLeft(20f);;
			
			Section section2 = firstChapter.addSection("second2");//子章节
			section2.setIndentationLeft(20f);
			section2.setNumberDepth(0); //不显示序号
			section2.add(new Phrase("second2   depth:"+section.getDepth()));
			document.add(firstChapter);
			//关闭document 以及异常捕获

在这里插入图片描述

随着子章节的增加,会发现标题前面的序号也越来越多。
2
2.1
2.1.1
2.1.2

默认情况下会创建书签,下图在chrome浏览器下的展示效果。
在这里插入图片描述

5.锚点相关

itext 中提供了 Anchor 来设置锚点,

一个简单的例子

Anchor anchor=new Anchor("我就是目标",font); //设置一个锚点, 锚点描述信息必须存在
anchor.setName("锚点名称");
docment.add(anchor);
document.newPage(); //另起一页

anchor=new Anchor("寻找目标",font); //指向锚点(锚点链接)
anchor.setReference("#锚点名称");
document.add(anchor);

脱坑指南

  1. 锚点名称可以是中文。
  2. 设置锚点时必须设置锚点描述信息(" "设置空字符串也没有用),否则无法跳转到该锚点。症状就是锚点链接可以出现小手,但点击无响应。
  3. 锚点链接必须设置 锚点描述信息。否则你看不到锚点链接。
  4. 锚点的名称(anchorName)和锚点链接的指向(#anchorName)必须一致,否则也会出现小手点击无响应
a.生成带有下划线的锚点链接

前面说过,锚点链接 必须设置描述信息(否则无法点击),但也并非只能在构造函数中传递参数。可以通过在anchor中添加元素实现。但是如果是通过构造函数则无法实现该部分的下划线显示。

	Anchor anchor =new Anchor();
	anchor.setReference("#target");
	Chunk chunk=new Chunk("目标地址你在哪??",font).setUnderline(0.2f, -font.getSize()*2/5);
	anchor.add(chunk);
	phrase.add(anchor);
b.对章节标题添加锚点
	Font firstChapterFont = new Font(baseFont,22,Font.BOLD,new BaseColor(6,105,185));
	Paragraph p =new Paragraph();//Chapter构造器不接受Anchor参数
	Anchor anchor=new Anchor(new Phrase("炼金术",firstChapterFont));
	anchor.setName("炼金术");
	p.setSpacingBefore(15f); //设置前后间距
	p.setSpacingAfter(15f);
	p.add(anchor);
	Chapter chapter=new Chapter(p,1);
	chapter.setTriggerNewPage(false);
	doc.add(chapter);

效果如下: 序号是用的是默认字体
在这里插入图片描述

可以在构造函数的时候,使用空字符串或者null同时指定字体样式。

		Font firstChapterFont = new Font(baseFont,22,Font.BOLD,new BaseColor(6,105,185));
		Paragraph p =new Paragraph("",firstChapterFont);//使用空字符串生成Paragraph对象,同时指定字体
		Anchor anchor=new Anchor();
		anchor.setName("炼金术");
		anchor.add(new Phrase("炼金术",firstChapterFont));
		p.setSpacingBefore(15f); //设置前后间距
		p.setSpacingAfter(15f);
		p.add(anchor);
		Chapter chapter=new Chapter(p,1);
		chapter.setTriggerNewPage(false);
		doc.add(chapter);

在这里插入图片描述

6. 自定义事件(添加页眉、水印、页码)

通过自定事件我们可以轻松的实现,添加页眉、水印、页码等功能。

import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;

public class PdfCustomEvent extends PdfPageEventHelper {
    private BaseFont baseFont;
    private int page=0;

    public PdfCustomEvent(BaseFont baseFont) {
        this.baseFont = baseFont;
    }

    @Override
    public void onStartPage(PdfWriter writer, Document document) {
        super.onStartPage(writer, document);
        page++;
    }

    @Override
    public void onEndPage(PdfWriter writer, Document document) {
        super.onEndPage(writer, document);

        PdfContentByte canvas = writer.getDirectContent();
        Rectangle rectangle =writer.getPageSize();
        Font headerFont = new Font(baseFont,10f,Font.NORMAL);

        ColumnText.showTextAligned(canvas, Element.ALIGN_RIGHT, new Phrase("页眉。。。。。。", headerFont),
                rectangle.getRight() - 35, rectangle.getTop() - 17, 0);// 页眉

        Font watermarkFont = new Font(baseFont,130,Font.NORMAL,new BaseColor(223,223,223,120));
        		   ColumnText.showTextAligned(writer.getDirectContentUnder(), Element.ALIGN_CENTER, new Phrase("水印", watermarkFont), rectangle.getRight()/2, rectangle.getTop()/2, 33);

        ColumnText.showTextAligned(canvas, Element.ALIGN_CENTER, new Phrase(""+page,headerFont),rectangle.getRight()/2 , rectangle.getBottom()+10, 0);//页脚
    }

}

 @RequestMapping("/viewPdf")
    public String testPdf(HttpServletRequest request, HttpServletResponse response) {
        OutputStream out = null;
        Document doc = new Document();
        try {
            BaseFont baseFont = BaseFont.createFont("pdf/MSYH.TTF", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
            Font font = new Font(baseFont, 12, Font.NORMAL);

            int chaterNum=1;
            out =response.getOutputStream();
            PdfWriter writer = PdfWriter.getInstance(doc, out);
            writer.setPageEvent(new PdfCustomEvent(baseFont));
            doc.open();

            Chapter chapter1=new Chapter(new Paragraph("企业信息",font),chaterNum++);
            Section businessInfo = chapter1.addSection(new Paragraph("工商信息",font));

            PdfPTable table = new PdfPTable(3);
            table.addCell(new Phrase("企业名称",font));
            table.addCell(new Phrase("注册时间",font));
            table.addCell(new Phrase("注册资本",font));
            table.addCell(new Phrase("哈哈哈1",font));
            table.addCell(new Phrase("2019-3-10",font));
            table.addCell(new Phrase("100万",font));
            table.addCell(new Phrase("哈哈哈1",font));
            table.addCell(new Phrase("2019-3-10",font));
            table.addCell(new Phrase("100万",font));
            table.addCell(new Phrase("哈哈哈1",font));
            table.addCell(new Phrase("2019-3-10",font));
            table.addCell(new Phrase("100万",font));
            businessInfo.add(table);
            doc.add(chapter1);
            
            doc.close();
        } catch (Exception e) {
            e.printStackTrace();
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }

        return null;
    }

上面样式有些丑,可以将章节标题字体加大,增大间距等。
在这里插入图片描述

7.实现带锚点的目录

上面已经介绍了 Chapter、Anchor和自定义事件。下面会将这些整合起来实现可以从目录标题跳转到指定标题的功能。

为标题添加锚点

使用标题作为锚点,因为通常情况下我们会添加的标题都是有序号的,这个序号是section中添加的。为了简单期间直接使用标题作为锚点

设置一级标题:

	 /**
     * 设置一级标题
     * @param num  序号
     * @param text  显示文本
     * @param font  字体
     * @return
     */
    public  Chapter setFirstChapterPadding(int num, String text, Font font) {
        //设置锚点
        Paragraph p =new Paragraph("",font);
        Anchor anchor=new Anchor();
        anchor.setName(text.trim()); //使用标题作为锚点
        anchor.add(new Phrase(text,font));
        p.setSpacingBefore(15f);
        p.setSpacingAfter(15f);
        p.add(anchor);
        //设置章节信息
        Chapter chapter=new Chapter(p,num);
        chapter.setTriggerNewPage(false);//不会另起一页
        return chapter;
    }

设置二级(多级)标题:

 /**
     * 设置一级标题
     * @param chapter 父章节
     * @param text  显示文本
     * @param font  字体
     * @return
     */
	public Section setSecondChapterPadding(Section chapter,String text,Font font) {
       Paragraph p = new Paragraph("",font);
       Anchor anchor=new Anchor();
       anchor.add(new Phrase(text,font));
       anchor.setName(text.trim());
       p.setSpacingBefore(10f);
       p.setSpacingAfter(10f);
       p.setMultipliedLeading(1.5f);
       p.add(anchor);
       Section section =chapter.addSection(p);
       return section;
    }
获取章节标题信息

该原有自定义事件在的基础上,增加了的章节标题信息的记录。

import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.draw.LineSeparator;
import java.util.*;
import java.util.List;

public class PdfCustomEvent extends PdfPageEventHelper {
    private BaseFont baseFont;
    private int page=0;
    private List<Map<String,Object>> chapters = new ArrayList<>(); //保存标题信息

    public PdfCustomEvent(BaseFont baseFont) {
        this.baseFont = baseFont;
    }

    @Override
    public void onStartPage(PdfWriter writer, Document document) {
        super.onStartPage(writer, document);
        page++;
    }
	
	// 页眉,页码,水印等
    @Override
    public void onEndPage(PdfWriter writer, Document document) {
        super.onEndPage(writer, document);

        PdfContentByte canvas = writer.getDirectContent();
        Rectangle rectangle =writer.getPageSize();
        Font headerFont = new Font(baseFont,13f,Font.NORMAL);

        ColumnText.showTextAligned(canvas, Element.ALIGN_CENTER, new Phrase("页眉。。。。。。", headerFont),
                rectangle.getRight() - 35, rectangle.getTop() - 17, 0);// 页眉
        ColumnText.showTextAligned(canvas, Element.ALIGN_CENTER, new Phrase(new Chunk(new LineSeparator())), rectangle.getRight()-40, rectangle.getTop()-40, 0);


        Font watermarkFont = new Font(baseFont,130,Font.NORMAL,new BaseColor(223,223,223,120));
        		   ColumnText.showTextAligned(writer.getDirectContentUnder(), Element.ALIGN_CENTER, new Phrase("水印", watermarkFont), rectangle.getRight()/2, rectangle.getTop()/2, 33);

        ColumnText.showTextAligned(canvas, Element.ALIGN_CENTER, new Phrase(""+page,headerFont),rectangle.getRight()/2 , rectangle.getBottom()+10, 0);//页脚
    }



    //添加新章节(chapter)的时候触发该方法
    @Override
    public void onChapter(PdfWriter writer, Document document, float paragraphPosition, Paragraph title) {
        //记录章节名称 以及对应的页数。 因为在这里面还涉及到了目录跳转到章节标题的代码,随意看起来有些多。
        Map<String,Object> tem=new HashMap<>();
        tem.put("page", page);
        tem.put("depth", 1);
        String[] resultStr = parseTitle(title);
        tem.put("title",resultStr[0]);
        tem.put("titleText",resultStr[1]);
        System.out.println("标题:\t("+resultStr[0]+"\t目标锚点("+resultStr[1]+")");
        chapters.add(tem);
    }

    //添加子章节(section)的时候触发该方法
    @Override
    public void onSection(PdfWriter writer, Document document, float paragraphPosition, int depth, Paragraph title) {
        //记录章节名称 以及对应的页数。 因为在这里面还涉及到了目录跳转到章节标题的代码,随意看起来有些多。
        Map<String,Object> tem=new HashMap<>();
        tem.put("page", page);
        tem.put("depth", depth);
        String[] resultStr = parseTitle(title);
        tem.put("title",resultStr[0]);
        tem.put("titleText",resultStr[1]);
        System.out.println("标题:\t("+resultStr[0]+"\t目标锚点("+resultStr[1]+")");
        chapters.add(tem);
    }
	
	 //解析标题,因为section对对标题增加序号,所以我们需要将序号去掉
    //比如 : 1.2. 股东信息-->分别用chunk保存  1.2.    股东信息
    private String[] parseTitle(Paragraph paragraph){
        String[] result=new String[2];
        Iterator<Element> elemensts =paragraph.iterator();

        //目标锚点,其实目标锚点和保存的章节标题内容大致相同。区别在于锚点内容不包含章节的序号。
        StringBuffer anchorBuffer = new StringBuffer();
        for(;elemensts.hasNext();){
            Element ele =elemensts.next();
            String str= null;
            if(ele instanceof Phrase) {
                str = ((Phrase)ele).getContent();
                System.out.println(((Phrase)ele).getContent());
            }else {
                //此处是有问题的,但是因为我的标题是纯文本,所以对我而言是没有问题。
                str= ele.toString().replaceAll("\\.", "").trim();
                System.out.println(ele);
            }
            try {
                Integer.valueOf(str);
            }catch (Exception e) {
                anchorBuffer.append(str);
            }
        }
        result[0] =paragraph.getContent();
        result[1]=anchorBuffer.toString();
        return result;
    }


     public List<Map<String,Object>> getChapterList(){
        return chapters;
     }
}

因为section会对我们添加的标题增加序号,序号和标题分别用一个Chunk保存。

1.2. 股东信息
1.2.
股东信息

为了方便代码阅,我将单元格设置样式、 创建目录的等代码提取到PdfUtils 类中。

import com.itextpdf.text.*;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfPageEvent;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.text.pdf.draw.DottedLineSeparator;
import java.util.List;
import java.util.Map;

public class PdfUtils {
    /**
     * 设置一级标题
     * @param num  序号
     * @param text  显示文本
     * @param font  字体
     * @return
     */
    public  Chapter setFirstChapterPadding(int num, String text, Font font) {
        //设置锚点
        Paragraph p =new Paragraph("",font);
        Anchor anchor=new Anchor();
        anchor.setName(text.trim());
        anchor.add(new Phrase(text,font));
        p.setSpacingBefore(15f);
        p.setSpacingAfter(15f);
        p.add(anchor);
        //设置章节信息
        Chapter chapter=new Chapter(p,num);
        chapter.setTriggerNewPage(false);//不会另起一页
        return chapter;
    }

    public Section setSecondChapterPadding(Section chapter,String text,Font font) {
        Paragraph p = new Paragraph("",font);
        Anchor anchor=new Anchor();
        anchor.add(new Phrase(text,font));
        anchor.setName(text.trim());
        p.setSpacingBefore(10f);
        p.setSpacingAfter(10f);
        p.setMultipliedLeading(1.5f);
        p.add(anchor);
        Section section =chapter.addSection(p);

        return section;
    }

    /**
     * 为表格添加标题
     * @param table
     * @param captionText
     */
    public void addTableCaption(PdfPTable table, Phrase captionText) {
        PdfPCell cell=new PdfPCell(captionText);
        cell.setPaddingBottom(20f);
        //对齐方式
        cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
        cell.setUseAscender(true);
        cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
        cell.setColspan(table.getNumberOfColumns());
        cell.setBorderWidth(0);
        cell.setPadding(3f);
        table.setWidthPercentage(100);
        table.addCell(cell);
    }

    public void addHeadCell(PdfPTable table,Phrase phrase){
        PdfPCell cell = new PdfPCell(phrase);
        cell.setBackgroundColor(new BaseColor(6,105,184));
        cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
        cell.setUseAscender(true);
        cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
        table.addCell(cell);
    }
	/**
	 * 创建目录
	 * @param writer
	 * @param document
	 * @param baseFont 中文字体
	 * @param env  自定义事件
	 * @throws DocumentException
	 */
    public  int createCatalog(PdfWriter writer, Document document, BaseFont baseFont, PdfPageEvent env)
            throws DocumentException {
        PdfCustomEvent event = (PdfCustomEvent) env;
        document.newPage();

        int tocNum = writer.getPageNumber();
        System.out.println("目录开始页数     \t" + tocNum);

        Font fontSize10 =new Font(baseFont,10.5f,Font.NORMAL);
        Paragraph catalogText = new Paragraph("目录", new Font(baseFont, 10.5f, Font.NORMAL,new BaseColor(0xff758fc8)));
        catalogText.setAlignment(Element.ALIGN_CENTER);
        document.add(catalogText);

        List<Map<String, Object>> lists = event.getChapterList();
        Paragraph catalogContent = new Paragraph();

        for (int i = 0; i < lists.size(); i++) {
            Map<String, Object> map = lists.get(i);
            int depth = (int) map.get("depth");
            int page = (int) map.get("page");
            String title = (String) map.get("title"); //获取(带有序号)标题
            String titleText=(String) map.get("titleText"); //获取(原生)标题
            if (depth == 1) {
            	// 指向目标锚点
                Anchor anchor=new Anchor();
                Paragraph p = new Paragraph(new Chunk(title, fontSize10));
                p.add(new Chunk(new DottedLineSeparator()));
                p.add(new Chunk("" + page));
                anchor.setReference("#"+titleText);
                anchor.add(p);
                catalogContent.add(anchor);
                catalogContent.add(new Phrase("\n"));
            } else {
                Anchor anchor=new Anchor();
                Paragraph paragraph = new Paragraph(new Chunk("    " + title, fontSize10));
                paragraph.add(new Chunk(new DottedLineSeparator()));
                paragraph.add(new Chunk("" + page));
                paragraph.setFirstLineIndent(18f);
                anchor.setReference("#"+titleText);
                anchor.add(paragraph);
                catalogContent.add(anchor);
                catalogContent.add(new Phrase("\n"));
            }
        }
        document.add(catalogContent);
        return tocNum;
    }
 /**
     * 对页面重排序
     * @date 2018年11月5日
     * @param orders
     *            所有的页面及其对应页码
     * @param inserPageNo
     *            目录页想要插入的页码。从1开始
     * @param catalogNo
     *            未排序前目录页的页码
     * @return void
     */
    public static void orderPage(int[] orders, int inserPageNo, int catalogNo) {
        if (orders == null)
            throw new NullPointerException();
        int totalPages = orders.length;
        inserPageNo -= 1;
        if (inserPageNo < 0 || inserPageNo >= totalPages)
            throw new IllegalArgumentException("index 参数范围不正确。当前插入页数"+inserPageNo+" 总页数"+totalPages);
        int pageNo = inserPageNo;
        for (int i = 0; i < inserPageNo; i++) {// 前 inserPageNo-1页对应页码不变
            orders[i] = i + 1;
        }
        for (; inserPageNo < totalPages; inserPageNo++) {
            orders[inserPageNo] = inserPageNo + catalogNo - pageNo;
            if (orders[inserPageNo] > totalPages)
                orders[inserPageNo] -= (totalPages - pageNo);
        }
    }
}


@RequestMapping("/viewPdf1")
    public String testPdf1(HttpServletRequest request, HttpServletResponse response) {
        OutputStream out = null;
        Document doc = new Document();
        try {
            BaseFont baseFont = BaseFont.createFont("pdf/font/MSYH.TTF", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
            Font font = new Font(baseFont, 12, Font.NORMAL);
            Font firstChapterFont = new Font(baseFont,22,Font.BOLD,new BaseColor(6,105,185));
            Font secondChapterFont = new Font(baseFont,16f,Font.BOLD,new BaseColor(6,105,185));

            int chaterNum=1;
            out =response.getOutputStream();
            PdfWriter writer = PdfWriter.getInstance(doc, out);
            PdfCustomEvent event=new PdfCustomEvent(baseFont);
            writer.setPageEvent(event);
            doc.open();

            PdfUtils pdfUtils=new PdfUtils();

            Chapter chapter1=pdfUtils.setFirstChapterPadding(chaterNum++,"企业信息",firstChapterFont);
            Section businessInfo = pdfUtils.setSecondChapterPadding(chapter1,"工商信息",secondChapterFont);

            PdfPTable table = new PdfPTable(3);
            pdfUtils.addTableCaption(table,new Phrase("企业信息:",font));
            pdfUtils.addHeadCell(table,new Phrase("企业名称",font));
            pdfUtils.addHeadCell(table,new Phrase("注册时间",font));
            pdfUtils.addHeadCell(table,new Phrase("注册资本",font));
            table.addCell(new Phrase("哈哈哈1",font));
            table.addCell(new Phrase("2019-3-10",font));
            table.addCell(new Phrase("100万",font));
            table.addCell(new Phrase("哈哈哈1",font));
            table.addCell(new Phrase("2019-3-10",font));
            table.addCell(new Phrase("100万",font));
            table.addCell(new Phrase("哈哈哈1",font));
            table.addCell(new Phrase("2019-3-10",font));
            table.addCell(new Phrase("100万",font));
            businessInfo.add(table);

            Section shareholderInfo = pdfUtils.setSecondChapterPadding(chapter1,"股东信息",secondChapterFont);
            PdfPTable shareholderTable = new PdfPTable(3);
            pdfUtils.addTableCaption(shareholderTable,new Phrase("股东信息:",font));
            pdfUtils.addHeadCell(shareholderTable,new Phrase("序号",font));
            pdfUtils.addHeadCell(shareholderTable,new Phrase("股东类型",font));
            pdfUtils.addHeadCell(shareholderTable,new Phrase("股东名称",font));
            shareholderTable.addCell(new Phrase("1",font));
            shareholderTable.addCell(new Phrase("自然股东",font));
            shareholderTable.addCell(new Phrase("股东名1",font));
            shareholderTable.addCell(new Phrase("2",font));
            shareholderTable.addCell(new Phrase("自然股东",font));
            shareholderTable.addCell(new Phrase("股东2",font));
            shareholderTable.addCell(new Phrase("3",font));
            shareholderTable.addCell(new Phrase("自然股东",font));
            shareholderTable.addCell(new Phrase("股东3",font));
            shareholderInfo.add(shareholderTable);
            doc.add(chapter1);

            Chapter chapter2=pdfUtils.setFirstChapterPadding(chaterNum++,"高管信息",firstChapterFont);
            PdfPTable mainTable = new PdfPTable(3);
            pdfUtils.addTableCaption(mainTable,new Phrase("高管信息表:",font));
            pdfUtils.addHeadCell(mainTable,new Phrase("序号",font));
            pdfUtils.addHeadCell(mainTable,new Phrase("职位",font));
            pdfUtils.addHeadCell(mainTable,new Phrase("姓名",font));
            mainTable.addCell(new Phrase("1",font));
            mainTable.addCell(new Phrase("小刘1",font));
            mainTable.addCell(new Phrase("董事",font));
            mainTable.addCell(new Phrase("2",font));
            mainTable.addCell(new Phrase("小刘3",font));
            mainTable.addCell(new Phrase("监事",font));
            chapter2.add(mainTable);
            doc.add(chapter2);

            doc.newPage();
            pdfUtils.createCatalog(writer,doc,baseFont,event);

            doc.close();
        } catch (Exception e) {
            e.printStackTrace();
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }

        return null;
    }

在这里插入图片描述

上面的代码并不完善,样式当然还需要再优化,再者就是目录不应该存在页码或者不应当和正文使用一个页码计数。

自定义排序

使用上面的自定义事件就可以保存我们的目录信息。通常我们会将这个保存的目录信息写到PDF末尾。这样就存在一个问题,怎么讲目录移到PDF文件的开头。

下面是itext 文档提供的代码
在这里插入图片描述

大致流程:

  1. 因为目录通常会单独新起一页,所以会先调用 newPage()方法另起一页。并获取当前的页码,这个就是目录的内容的开始页码

  2. 获取页码以后,就需要向文档中添加目录内容。(这一部分就需要自定义了,因为需求不同,显示的内容也就不尽相同)

  3. 在获取你需要排序的总页数之前(reorderPages()),需要先调用newPage()

  4. 创建一个int数组用来保存重排序的页码。(这一部分也是需要自定义,因为官方提供的只是将目录放在第一页,而且目录前并没有内容,通常来说在目录前可能会添加一些报告说明或者摘要等信息)

  5. 调用reorderPages(order),完成页面的排序。
    注意: 使用该方式重排序页面需要在打开文档之前调用代码 writer.setLinearPageMode();

    writer.setLinearPageMode();
    document.open();
    

    默认情况下Itext使用平衡数来组织页面结构已达到较好的性能,但是如果想要页面排序,那么就只能调用setLinearPageMode 使用一种简单的结构。

在这里插入图片描述

在之前的基础上做一下修改就可以了。

writer.setLinearPageMode();
document.open();
..........................................
..........................................

int catalogNum = pdfUtils.createCatalog(writer,doc,baseFont,event);
doc.newPage();
int totalPage = writer.reorderPages(null);//获取总页数
int[] order = new int[totalPage];
pdfUtils.orderPage(order,1, catalogNum);//调整书签位置
writer.reorderPages(order);//排序

在这里插入图片描述

8. 图片相关
问题
  1. 图片分页,导致图片下方的说明和图片不在同一页,而且上级次序颠倒
    图片中的问题就是使用常规的方式添加一种图片,并添加文本说明导致的问题。因为还使用自定义事件完成页眉的添加,所以页眉的位置也位置不对
    在这里插入图片描述
    解决方案,图片和文件说明添加到PdfPTable中,就可以解决该问题
    下面是修改完的部分代码

    Section controllerSection=setSecondChapterPadding(chapter,"直接控制人", getSecondChapterFont()); //直接控制人二级标题
    if(controlPersonPicture!=null) {//图片可以是数据库获取,或者ClassPathResource获取本地文件
             //修改图片的一些配置
            controlPersonPicture.setAlignment(Image.ALIGN_CENTER);
            float imageWidth = controlPersonPicture.getWidth();
            float imageHeight = controlPersonPicture.getHeight();
            controlPersonPicture.scaleAbsolute(mmTopx(145), mmTopx(145 / imageWidth * imageHeight));//调整图片的相对大小
            
            //将图片添加到表格中
            PdfPTable table=new PdfPTable(1);
            table.setWidthPercentage(100);
            PdfPCell cell=new PdfPCell(companyFamilyPicture);
            cell.setBorderWidth(0); //边框设置为0
            table.addCell(cell);
            cell=new PdfPCell(new Phrase("图 "+chapterNum+"-1"+": 直接控制人图",getTableCaptionFont()));//图片说明
            cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);//居中显示
            cell.setBorderWidth(0);
            table.addCell(cell);
            controllerSection.add(table);
    }
    

    修改完成后的显示如下
    在这里插入图片描述

读取文档

PdfReader

重要的方法:

getNumberOfPages();// 获取文档有多少页
getPageSizeWithRotation(;// getPageSize() 和 getPageRotation()的结合体
  • 损坏的pdf文件是不能打开的。

  • 如果文档进行了加密,也是不能打开的(特指用户密码)

读取文档时减少内存使用。
  1. 读取全部文档

    PdfReader reader= new PdfReader(filename);
    
  2. 读取部分文件

    PdfReader reader = new PdfReader(new RandomAccessFileOrArray(filename), null);
    

When reading a file partially, more memory will be used as soon as you start working with the reader object, but PdfReader won’t cache unnecessary objects
3. 在开始使用PdfReader之前,减少页数

reader.selectPages("4-8"); //读取4到8页
//[!][o][odd][e][even]start[-end

selectPages的语法。

[!]   [o]  [odd]  [e ]  [even]   start   [-end]
  • 可以同时指定多个返回使用逗号隔开,“4-8,9-11”。范围必须是递增的。
  • 可以省略 start 或者 end。
  • 如果同时省略start 或者end 。需要指定 o(奇数页)或者 e(偶数页)
  • 使用! 可以删除之前已被选中的范围
  • 选中范围以后,页面将从1 开始计数。4-8,则第四页为新的第一页
读取pdf
从已存在的文档中拷贝页面

In this section, you’ll use an object named PdfImportedPage to copy the content from an existing PDF opened with PdfReader into a new Document written by PdfWriter.

如果想要重新使用某个文档的页面,并将这些页面看作图片。

Document document = new Document();
PdfWriter writer=PdfWriter.getInstance(document,path);
PdfReader reader= new PdfReader(path);
int total = reader.getNumberOfPages();
PdfImportedPage importedPage = null;
for(int i=0;i<total;i++){
	page = writer.getImportedPage(reader, i);
	table.addCell(Image.getInstance(page));
}

参考文献:

  1. iText in action
  2. 百度百科
评论 56
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值