iText 目前有两套版本,分别是 iText5 和 iText7。iText5 应该是网上用的比较多的一个版本。iText5 因为是很多开发者参与贡献代码,因此在一些规范和设计上存在不合理的地方。iText7 是后来官方针对 iText5 的重构,两个版本差别还是挺大的。不过在实际使用中,一般用到的都比较简单的 API,所以不用特别拘泥于使用哪个版本。
想要的效果
相关依赖
<!-- PDF操作 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
<!-- 支持中文 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
测试用例
import cn.hutool.core.date.DateUtil;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class PdfService {
public static void main(String[] args) throws Exception {
PdfBaseWriter writer = new PdfBaseWriter("F:/test3.pdf");
writer.open();
writer.add(writer.getParagraph(4, " "));
writer.add(writer.getParagraph(1, "××××××调查表"));
Paragraph paragraph = writer.getParagraph(2, "(×××年度)");
paragraph.setAlignment(Paragraph.ALIGN_CENTER);
writer.add(paragraph);
Paragraph paragraph1 = writer.getParagraph(3, "×××全称(盖章):来了老弟");
paragraph1.setAlignment(Paragraph.ALIGN_CENTER);
writer.add(paragraph1);
Paragraph paragraph2 = writer.getParagraph(3, "填报时间: ××× 年 ×× 月 ×× 日");
paragraph2.setAlignment(Paragraph.ALIGN_CENTER);
writer.add(paragraph2);
Map<String, String> map = new HashMap<>();
map.put("甲方:", "xxxxx科技有限公司");
map.put("乙方:", "xxxx");
map.put("时间:", String.valueOf(DateUtil.date()));
map.put("地点:", "想在哪里就在那里");
writer.writeList(map, false);
writer.writeText("《根据中华人民共和国合同法》的有关规定,经甲、乙双方友好协商,本着长期平等合作.....");
ArrayList<LinkedHashMap<String, Object>> rows = new ArrayList<>();
for (int i = 0; i < 5; i++) {
LinkedHashMap<String, Object> row = new LinkedHashMap<>();
row.put("姓名", "张三"+i);
row.put("年龄", 23);
row.put("成绩", 88.32);
row.put("是否合格", true);
row.put("考试日期", DateUtil.date());
rows.add(row);
}
writer.writeTable(rows);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL resource = classLoader.getResource("image/test.jpg");
Image image = Image.getInstance(resource);
image.scaleToFit(500, 200);
image.setAlignment(Image.MIDDLE);
writer.add(image);
writer.setTitle1("合作方式及条件", 1);
ArrayList<String> list = new ArrayList();
list.add("双方根据国家法律规定建立合作关系,双方严格遵守和执行国家各项方针政策和有关法律、法规和条例规定。 ");
list.add("双方严格按照《中华人民共和国招标投标法》及相关规定实施合作。");
list.add("双方本着密切配合、分工协作、保证质量、按期完成的原则,共同做好工作。");
writer.writeList(list, true);
writer.setTitle1("权利义务", 2);
writer.writeList(list, true);
writer.setTitle1("其它", 3);
writer.writeList(list, true);
PdfPTable table = new PdfPTable(2);
table.setWidthPercentage(100);
table.setSpacingBefore(10);
writer.writeNoBorderCell("甲方:(盖章)", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("乙方:(盖章)", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("法定代表人或负责人签章", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("法定代表人或负责人签章", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("地址:", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("地址:", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("开户银行:", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("开户银行:", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("邮编:", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("邮编:", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("授权代理人:", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("项目经理:", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("电话:", Element.ALIGN_LEFT, 1, 1, table);
writer.writeNoBorderCell("电话:", Element.ALIGN_LEFT, 1, 1, table);
writer.add(table);
URL sealRes = classLoader.getResource("image/test_seal.jpeg");
writer.setWatermark("一个不称职的程序猿");
writer.setStamp("盖章", sealRes);
// PDF文档属性
writer.addTitle("基本PDF导出");// 标题
writer.addAuthor("作者");// 作者
writer.addSubject("基本PDF导出");// 主题
writer.addKeywords("基本PDF导出");// 关键字
writer.addCreator("XXXX有限公司");// 谁创建的
writer.close();
System.out.println("执行完了!");
}
}
小工具
import com.itextpdf.awt.geom.Rectangle2D;
import com.itextpdf.text.*;
import com.itextpdf.text.Font;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import com.itextpdf.text.pdf.parser.RenderListener;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import lombok.extern.slf4j.Slf4j;
import javax.swing.*;
import java.awt.*;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>
* PDF 写入器
* </p>
*
* @Author REID
* @Blog <a href="https://blog.csdn.net/qq_39035773">Blog</a>
* @GitHub <a href="https://github.com/BeginnerA">GitHub</a>
* @Version V1.0
**/
@Slf4j
public class PdfBaseWriter extends Document {
//------------------------------------默认参数------------------------------------
/**
* 中文字体
*/
public final static String FONT_NAME_ST_SONG_LIGHT = "STSong-Light";
/**
* 中文字体编码
*/
public final static String FONT_ENCODING_UNI_GB_UCS2_H = "UniGB-UCS2-H";
//------------------------------------常用字体/颜色------------------------------------
/**
* 灰色
*/
public final static BaseColor GREY_COLOR = new BaseColor(139, 134, 130);
/**
* 浅黄色
*/
public final static BaseColor LIGHT_YELLOW_COLOR = new BaseColor(255, 255, 153);
/**
* 浅蓝色
*/
public final static BaseColor LIGHT_BLUE_COLOR = new BaseColor(133, 188, 224);
/**
* 深蓝色
*/
public final static BaseColor HARD_BLUE_COLOR = new BaseColor(31, 86, 112);
/**
* 写出 PDF 文件路径
*/
private String destFilePath;
/**
* 创建页面大小为 A4 的 PDF {@link Document}
* @param destFilePath PDF 存放路径
* @throws DocumentException 创建 PDF 流失败
*/
public PdfBaseWriter(String destFilePath) throws DocumentException {
super();
this.destFilePath = destFilePath;
PdfWriter.getInstance(this, getPdfOutputStream(destFilePath));
}
/**
* 创建 PDF {@link Document}
* @param destFilePath PDF 存放路径
* @param PageSize 页面大小 {@link Rectangle}
* @throws DocumentException 创建 PDF 流失败
*/
public PdfBaseWriter(String destFilePath, Rectangle PageSize) throws DocumentException {
super(PageSize);
this.destFilePath = destFilePath;
PdfWriter.getInstance(this, getPdfOutputStream(destFilePath));
}
//------------------------------------表------------------------------------
/**
* 写入表格,表格数据集合中第一行数据 key 为表格表头
* @param rows 表格数据集合
* @return 表格 {@link PdfPTable}
*/
public PdfPTable writeTable(ArrayList<LinkedHashMap<String, Object>> rows) {
if (rows == null) {
return null;
}
// 根据写入数据宽度创建表格
List<String> headers = new ArrayList<String>(rows.get(0).keySet());
PdfPTable table = createWithHeaderTable(headers);
for (LinkedHashMap<String, Object> row : rows) {
for (String k : headers) {
String text = String.valueOf(row.get(k));
writeCell(String.valueOf(text), calculateColumnNumber(k), 1, table);
}
}
addContent(table);
return table;
}
/**
* 创建带表头的表格 {@link PdfPTable}
* @param headers 表头数据集
* @return 表格 {@link PdfPTable}
*/
public PdfPTable createWithHeaderTable(List<String> headers) {
int numColumns = calculateColumnNumber(headers);
PdfPTable headerTable = new PdfPTable(numColumns);
headerTable.setWidthPercentage(100);
headerTable.setSpacingBefore(10);
for (String text : headers) {
writeCell(text, Element.ALIGN_CENTER, calculateColumnNumber(text), 1, headerTable, defaultFont(), 0f, Rectangle.BOX, LIGHT_BLUE_COLOR);
}
return headerTable;
}
/**
* 设置空白行
* @param table 表 {@link PdfPTable}
* @param bgColor 背景颜色 {@link BaseColor}
* @param columnNumber 列号
* @param fixedHeight 固定高度
*/
public void setBlankRow(PdfPTable table, BaseColor bgColor, int columnNumber, float fixedHeight) {
Paragraph paragraph = new Paragraph("");
PdfPCell pdfPCell = PdfUtil.newPdfPCell(bgColor, 0, 1, columnNumber, 1, paragraph);
// 单元格固定高度
pdfPCell.setFixedHeight(fixedHeight);
table.addCell(pdfPCell);
}
//------------------------------------单元格------------------------------------
/**
* 写入无边框单元格 {@link PdfPCell}
* @param text 内容
* @param align 对齐方式 {@link Element}
* @param colspan 所占列数
* @param rowspan 所占行数
* @param table 表 {@link PdfPTable}
* @return 单元格 {@link PdfPCell}
*/
public PdfPCell writeNoBorderCell(String text, int align, int colspan, int rowspan, PdfPTable table) {
return writeCell(text, align, colspan, rowspan, table, defaultFont(), 0f, Rectangle.NO_BORDER, BaseColor.WHITE);
}
/**
* 写入单元格 {@link PdfPCell}
* @param text 内容
* @param colspan 所占列数
* @param rowspan 所占行数
* @param table 表 {@link PdfPTable}
* @return 单元格 {@link PdfPCell}
*/
public PdfPCell writeCell(String text, int colspan, int rowspan, PdfPTable table) {
return writeCell(text, Element.ALIGN_CENTER, colspan, rowspan, table, defaultFont(), 0f, Rectangle.BOX, BaseColor.WHITE);
}
/**
* 写入单元格 {@link PdfPCell}
* @param text 内容
* @param align 对齐方式 {@link Element}
* @param colspan 所占列数
* @param rowspan 所占行数
* @param table 表 {@link PdfPTable}
* @param font 字体 {@link Font}
* @return 单元格 {@link PdfPCell}
*/
public PdfPCell writeCell(String text, int align, int colspan, int rowspan, PdfPTable table, Font font) {
return writeCell(text, align, colspan, rowspan, table, font, 0f, Rectangle.BOX);
}
/**
* 写入单元格 {@link PdfPCell}
* @param text 内容
* @param align 对齐方式 {@link Element}
* @param colspan 所占列数
* @param rowspan 所占行数
* @param table 表 {@link PdfPTable}
* @param font 字体 {@link Font}
* @param paddingLeft 左边距
* @param border 边框 {@link Rectangle}
* @return 单元格 {@link PdfPCell}
*/
public PdfPCell writeCell(String text, int align, int colspan, int rowspan, PdfPTable table, Font font, Float paddingLeft, int border) {
return writeCell(text, align, colspan, rowspan, table, font, paddingLeft, border, true, BaseColor.WHITE);
}
/**
* 写入单元格 {@link PdfPCell}
* @param text 内容
* @param align 对齐方式 {@link Element}
* @param colspan 所占列数
* @param rowspan 所占行数
* @param table 表 {@link PdfPTable}
* @param font 字体 {@link Font}
* @param paddingLeft 左边距
* @param border 边框 {@link Rectangle}
* @param bgColor 背景颜色 {@link BaseColor}
* @return 单元格 {@link PdfPCell}
*/
public PdfPCell writeCell(String text, int align, int colspan, int rowspan, PdfPTable table, Font font, Float paddingLeft, int border, BaseColor bgColor) {
return writeCell(text, align, colspan, rowspan, table, font, paddingLeft, border, true, bgColor);
}
/**
* 写入单元格 {@link PdfPCell}
* @param text 内容
* @param align 对齐方式 {@link Element}
* @param colspan 所占列数
* @param rowspan 所占行数
* @param table 表 {@link PdfPTable}
* @param font 字体 {@link Font}
* @param paddingLeft 左边距
* @param border 边框 {@link Rectangle}
* @param isSet 是否已经设置
* @param bgColor 背景颜色 {@link BaseColor}
* @return 单元格 {@link PdfPCell}
*/
public PdfPCell writeCell(String text, int align, int colspan, int rowspan, PdfPTable table, Font font, Float paddingLeft, int border, boolean isSet, BaseColor bgColor) {
if (text == null) {
text = "";
}
// 包含中文,则使用中文字体
if (isChinese(text)) {
font = chineseFont(font.getSize(), font.getStyle(), font.getColor());
}
Paragraph elements = new Paragraph(text, font);
elements.setAlignment(align);
PdfPCell cell = newPdfPCell(bgColor, border, align, colspan, rowspan, elements);
if (paddingLeft != null) {
cell.setPaddingLeft(paddingLeft);
}
if (isSet) {
table.addCell(cell);
}
return cell;
}
/**
* 创建新的单元格 {@link PdfPCell}
* @param bgColor 背景颜色 {@link BaseColor}
* @param border 边框 {@link Rectangle}
* @param align 对齐方式 {@link Element}
* @param colspan 所占列数
* @param rowspan 所占行数
* @param paragraph 段落 {@link Paragraph}
* @return 单元格 {@link PdfPCell}
*/
public PdfPCell newPdfPCell(BaseColor bgColor, int border, int align, int colspan, int rowspan, Paragraph paragraph) {
PdfPCell cell = new PdfPCell();
cell.setBorderColor(BaseColor.BLACK);
cell.setBorderColorLeft(BaseColor.BLACK);
cell.setBorderColorRight(BaseColor.BLACK);
cell.setBackgroundColor(bgColor);
cell.setBorder(border);
// 水平居中
cell.setHorizontalAlignment(align);
// 垂直居中
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
cell.setColspan(colspan);
cell.setRowspan(rowspan);
cell.setMinimumHeight(10);
cell.addElement(paragraph);
// 设置单元格的边框宽度和颜色
cell.setBorderWidth(0.5f);
cell.setBorderColor(GREY_COLOR);
return cell;
}
/**
* 创建内容为图片的单元格 {@link PdfPCell}
* @param bgColor 背景 {@link BaseColor}
* @param border 边框 {@link Rectangle}
* @param align 对齐方式 {@link Element}
* @param colspan 所占列数
* @param rowspan 所占行数
* @param image 内容(文字或图片对象) {@link Image}
* @return 单元格 {@link PdfPCell}
*/
public PdfPCell newPdfPCellOfImage(BaseColor bgColor, int border, int align, int colspan, int rowspan, Image image) {
PdfPCell cell = new PdfPCell();
cell.setBorderColor(BaseColor.BLACK);
cell.setBorderColorLeft(BaseColor.BLACK);
cell.setBorderColorRight(BaseColor.BLACK);
cell.setBackgroundColor(bgColor);
cell.setBorder(border);
cell.setUseAscender(Boolean.TRUE);
cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
cell.setHorizontalAlignment(align);
cell.setColspan(colspan);
cell.setRowspan(rowspan);
cell.addElement(image);
return cell;
}
//------------------------------------文本内容------------------------------------
/**
* 设置一级标题
* @param title 章节标题
* @param number 章节编号
* @return 返回当前标题 {@link Chapter}
*/
public Chapter setTitle1(String title, int number) {
Paragraph paragraph = getParagraph(2, title);
Chapter chapter = new Chapter(paragraph, number);
chapter.setTriggerNewPage(false);
addContent(chapter);
return chapter;
}
/**
* 设置二级标题
* @param chapter 对应一级标题 {@link Chapter}
* @param title 章节标题
* @return 返回当前标题 {@link Section}
*/
public Section setTitle2(Chapter chapter, String title) {
Paragraph paragraph = getParagraph(3, title);
Section section = chapter.addSection(paragraph);
section.setNumberStyle(Section.NUMBERSTYLE_DOTTED_WITHOUT_FINAL_DOT);
addContent(section);
return section;
}
/**
* 设置三级标题
* @param section 对应二级标题 {@link Section}
* @param title 章节标题
*/
public void setTitle3(Section section, String title) {
Paragraph paragraph = getParagraph(4, title);
Section subsection = section.addSection(paragraph);
subsection.setNumberStyle(Section.NUMBERSTYLE_DOTTED);
addContent(subsection);
}
/**
* 写出段落正文
* @param text 正文内容
*/
public void writeText(String text) {
writeText(text, defaultFont(), true);
}
/**
* 写出段落正文
* @param text 正文内容
* @param font 字体 {@link Font}
*/
public void writeText(String text, Font font) {
writeText(text, font, true);
}
/**
* 写出段落正文
* @param text 正文内容
* @param font 字体 {@link Font}
* @param firstLineIndent 首行是否需要缩进
*/
public void writeText(String text, Font font, boolean firstLineIndent) {
Paragraph paragraph = new Paragraph(text, font);
if (firstLineIndent) {
paragraph.setFirstLineIndent(24);
}
addContent(paragraph);
}
/**
* 写出段落正文
* @param text 正文内容
* @param font 字体 {@link Font}
* @param alignment 设置此段落的对齐方式 {@link Element}
*/
public void writeText(String text, Font font, int alignment) {
Paragraph paragraph = new Paragraph(text, font);
paragraph.setAlignment(alignment);
addContent(paragraph);
}
/**
* 写出列表
* @param texts 列表内容集合
* @param wantNumbering 是否需要序号
*/
public void writeList(List<String> texts, boolean wantNumbering) {
if (texts == null || texts.size() == 0) {
return;
}
int i = 1;
for (String text : texts) {
if (wantNumbering) {
writeText(i + "、" + text, defaultFont(), false);
}else {
writeText(text, defaultFont(), false);
}
i++;
}
}
/**
* 写出列表
* @param formData 列表内容集合
* @param tagValue value 值是否需要下划线
*/
public void writeList(Map<String, String> formData, boolean tagValue) {
if (formData == null || formData.size() == 0) {
return;
}
for (String v : formData.keySet()) {
addContent(new Phrase(v + ":", defaultFont()));
Phrase phrase = new Phrase();
Chunk chunk = new Chunk(formData.get(v), defaultFont());
if (tagValue) {
chunk.setUnderline(0.2f, -2f);
}
phrase.add(chunk);
addContent(phrase);
addContent(Chunk.NEWLINE);
}
}
/**
* 创建文本
* @param type 1-标题 2-标题一 3-标题二 4-标题三 5-正文 6-正文左对齐
*/
public Paragraph getParagraph(int type, String text) {
Font font = new Font(defaultFont());
if (1 == type) {
font.setSize(22f);
font.setStyle(Font.BOLD);
} else if(2 == type) {
font.setSize(16f);
font.setStyle(Font.BOLD);
} else if(3 == type) {
font.setSize(14f);
font.setStyle(Font.BOLD);
} else if(4 == type) {
font.setSize(14f);
} else if(5 == type) {
font.setSize(10.5f);
} else if(6 == type) {
font.setSize(10.5f);
} else {
font.setSize(10.5f);
}
Paragraph paragraph = new Paragraph(text, font);
if (1 == type) {
paragraph.setAlignment(Paragraph.ALIGN_CENTER);
paragraph.setSpacingBefore(10f);
paragraph.setSpacingAfter(10f);
} else if(2 == type) {
paragraph.setAlignment(Element.ALIGN_JUSTIFIED);
paragraph.setSpacingBefore(2f);
paragraph.setSpacingAfter(2f);
} else if(3 == type){
paragraph.setSpacingBefore(2f);
paragraph.setSpacingAfter(1f);
} else if(4 == type){
paragraph.setSpacingBefore(2f);
paragraph.setSpacingAfter(2f);
} else if(5 == type){
paragraph.setAlignment(Element.ALIGN_JUSTIFIED);
paragraph.setFirstLineIndent(24);
paragraph.setSpacingBefore(1f);
paragraph.setSpacingAfter(1f);
} else if(6 == type){
paragraph.setAlignment(Element.ALIGN_LEFT);
paragraph.setSpacingBefore(1f);
paragraph.setSpacingAfter(1f);
}
return paragraph;
}
/**
* 添加 PDF 内容
* @param element 要添加的 {@link Element}
* @return 如果添加了元素,则为true ,否则为false
*/
public boolean addContent(Element element) {
try {
return this.add(element);
} catch (DocumentException e) {
log.error("PDF 文档尚未打开或已关闭");
throw new RuntimeException(e);
}
}
//------------------------------------水印------------------------------------
/**
* 水印
*/
private String watermark;
/**
* 印章位置关键字
*/
private String stampPositionKeyword;
/**
* 印章文件路径
*/
private Image stamp;
/**
* 阅读 PDF 文档
*/
PdfReader reader = null;
/**
* 将额外内容应用到 PDF 文档的页面。这个额外的内容可以是 PdfContentByte 中允许的所有对象,包括来自其他 Pdfs 的页面。<br>
* 原始 PDF 将保留所有交互元素,包括书签、链接和表单域。<br>
* 也可以更改字段值并将它们展平。可以添加新字段,但不能展平。<br>
*/
PdfStamper stamper = null;
/**
* 设置水印内容
* @param watermark 水印内容
*/
public void setWatermark(String watermark) {
if (watermark != null && watermark.trim().length() > 0) {
this.watermark = watermark;
}
}
/**
* 设置印章
* @param stampPositionKeyword 印章位置关键字,根据关键字会计算出印章所在坐标
* @param stampFilePath 印章文件路径
*/
public void setStamp(String stampPositionKeyword, String stampFilePath) {
try {
if (stampPositionKeyword != null && stampPositionKeyword.trim().length() > 0) {
this.stampPositionKeyword = stampPositionKeyword;
this.stamp = Image.getInstance(stampFilePath);
}
} catch (MalformedURLException e) {
log.error("添加印章失败!错误信息为: ", e);
this.stampPositionKeyword = null;
this.stamp = null;
throw new RuntimeException(e);
} catch (BadElementException e) {
log.error("添加印章失败!错误信息为: ", e);
this.stampPositionKeyword = null;
this.stamp = null;
throw new RuntimeException(e);
} catch (IOException e) {
log.error("添加印章失败!错误信息为: ", e);
this.stampPositionKeyword = null;
this.stamp = null;
throw new RuntimeException(e);
}
}
/**
* 设置印章
* @param stampPositionKeyword 印章位置关键字,根据关键字会计算出印章所在坐标
* @param stampFilePath 印章文件路径
*/
public void setStamp(String stampPositionKeyword, URL stampFilePath) {
try {
if (stampPositionKeyword != null && stampPositionKeyword.trim().length() > 0) {
this.stampPositionKeyword = stampPositionKeyword;
this.stamp = Image.getInstance(stampFilePath);
}
} catch (MalformedURLException e) {
log.error("添加印章失败!错误信息为: ", e);
this.stampPositionKeyword = null;
this.stamp = null;
throw new RuntimeException(e);
} catch (BadElementException e) {
log.error("添加印章失败!错误信息为: ", e);
this.stampPositionKeyword = null;
this.stamp = null;
throw new RuntimeException(e);
} catch (IOException e) {
log.error("添加印章失败!错误信息为: ", e);
this.stampPositionKeyword = null;
this.stamp = null;
throw new RuntimeException(e);
}
}
/**
* 添加水印
* @param text 水印文本内容
* @param opacity 水印字体透明度
* @param fontsize 水印字体大小
* @param heightDensity 数值越大每页竖向水印越少
* @param widthDensity 数值越大每页横向水印越少
* @param angle 水印倾斜角度(0-360)
*/
private void setWatermark(String text, float opacity, float fontsize, float heightDensity, float widthDensity, float angle) {
try {
PdfGState gs = new PdfGState();
// 设置图片透明度
gs.setFillOpacity(opacity);
// 设置笔触字体不透明度
gs.setStrokeOpacity(0.1f);
// 水印字体
BaseFont font = BaseFont.createFont(FONT_NAME_ST_SONG_LIGHT, FONT_ENCODING_UNI_GB_UCS2_H, BaseFont.EMBEDDED);
// 页面宽高
Rectangle pageRect = reader.getPageSizeWithRotation(1);
float pageHeight = pageRect.getHeight();
float pageWidth = pageRect.getWidth();
// 水印内容宽高
JLabel label = new JLabel();
FontMetrics metrics;
label.setText(text);
metrics = label.getFontMetrics(label.getFont());
int textH = metrics.getHeight();
int textW = metrics.stringWidth(label.getText());
int total = reader.getNumberOfPages();
for (int i = 1; i <= total; i++) {
/*
* PDF 分为四层,第一层和第四层由低级操作来进行操作,第二层、第三层由高级对象操作(从下往上)
* 第一层操作只能使用 PdfWriter.DirectContent 操作,第四层使用 DirectContentUnder 操作,。
* 第二层和第三层的 PdfContentByte 是由 IText 内部操作,没有提供 api 接口。
*/
PdfContentByte waterMar = stamper.getOverContent(i);
waterMar.beginText();
// 设置水印颜色
waterMar.setColorFill(BaseColor.GRAY);
waterMar.setGState(gs);
waterMar.setFontAndSize(font, fontsize);
// 设置水印对齐方式 水印内容 X坐标 Y坐标 旋转角度
for (float height = textH; height < pageHeight * 2; height = height + textH * heightDensity) {
for (float width = textW; width < pageWidth * 1.5 + textW; width = width + textW * widthDensity) {
waterMar.showTextAligned(Element.ALIGN_LEFT, text, width - textW, height - textH, angle);
}
}
waterMar.endText();
}
} catch (DocumentException e) {
log.error("设置水印失败!错误信息为: ", e);
throw new RuntimeException(e);
} catch (IOException e) {
log.error("设置水印失败!错误信息为: ", e);
throw new RuntimeException(e);
}
}
/**
* 设置图片水印
*/
private void setImageWatermark() {
try {
List<float[]> keywordCoord = getKeywordCoord();
for (float[] coord : keywordCoord) {
PdfContentByte under = stamper.getUnderContent((int)coord[2]);
stamp.setAbsolutePosition(coord[0], coord[1] - 50);
under.addImage(stamp);
// TODO: 2022/5/26 印章功能还没搞定,等有时间在接着搞
PdfDiv sealImageDiv = new PdfDiv();
sealImageDiv.setPosition(PdfDiv.PositionType.ABSOLUTE);
sealImageDiv.setPaddingLeft(coord[0]);
sealImageDiv.setPaddingBottom(coord[1] - 50);
stamp.scaleAbsolute(100f, 100f);
sealImageDiv.addElement(stamp);
}
} catch (DocumentException e) {
log.error("设置图片水印失败!错误信息为: ", e);
throw new RuntimeException(e);
}
}
/**
* 获取印章坐标,可能多个位置需要设置印章
* @return 印章坐标
*/
private List<float[]> getKeywordCoord() {
try {
List<float[]> coordList = new ArrayList<>();
int pageNum = reader.getNumberOfPages();
PdfReaderContentParser pdfReaderContentParser = new PdfReaderContentParser(reader);
// 下标从1开始
for (int i = 1; i <= pageNum; i++) {
int finalI = i;
pdfReaderContentParser.processContent(i, new RenderListener() {
public void renderText(TextRenderInfo textRenderInfo) {
String text = textRenderInfo.getText();
if (null != text && text.contains(stampPositionKeyword)) {
Rectangle2D.Float boundingRectange = textRenderInfo.getBaseline().getBoundingRectange();
float[] coord = new float[3];
coord[0] = boundingRectange.x;
coord[1] = boundingRectange.y;
coord[2] = finalI;
coordList.add(coord);
}
}
public void renderImage(ImageRenderInfo arg0) {
}
public void endTextBlock() {
}
public void beginTextBlock() {
}
});
}
return coordList;
} catch (IOException e) {
log.error("设置图片水印失败!错误信息为: ", e);
throw new RuntimeException(e);
}
}
//------------------------------------字体------------------------------------
/**
* 默认中文字体<br>
* 大小:12<br>
* 样式:正常<br>
* 颜色:黑色<br>
* @return 中文字体 {@link Font}
*/
public Font defaultFont() {
return chineseFont(10.5f, Font.NORMAL, BaseColor.BLACK);
}
/**
* 默认中文字体
* @return 中文字体 {@link BaseFont}
*/
public BaseFont defaultBaseFont() {
try {
return BaseFont.createFont(FONT_NAME_ST_SONG_LIGHT, FONT_ENCODING_UNI_GB_UCS2_H, BaseFont.EMBEDDED);
} catch (DocumentException e) {
log.error("获取无效字体");
throw new RuntimeException(e);
} catch (IOException e) {
log.error("无法读取字体文件");
throw new RuntimeException(e);
}
}
/**
* 中文字体 {@link Font}
* @param fontSize 字体大小
* @param style 字体风格 {@link Font}
* @param color 字体颜色 {@link BaseColor}
* @return {@link Font}
*/
public Font chineseFont(float fontSize, int style, BaseColor color) {
BaseFont baseFont = null;
try {
baseFont = BaseFont.createFont(FONT_NAME_ST_SONG_LIGHT, FONT_ENCODING_UNI_GB_UCS2_H, BaseFont.EMBEDDED);
} catch (DocumentException e) {
log.error("获取无效字体");
throw new RuntimeException(e);
} catch (IOException e) {
log.error("无法读取字体文件");
throw new RuntimeException(e);
}
return createFont(baseFont, fontSize, style, color);
}
/**
* 自定义中文字体 {@link Font}
* @param fontPath 字体格式文件路径(如:.ttc、.ttf),支持绝对路径或项目静态资源相对路径
* @param fontSize 字体大小
* @param style 字体风格 {@link Font}
* @param color 字体颜色 {@link BaseColor}
* @return {@link Font}
*/
public Font customFont(String fontPath, float fontSize, int style, BaseColor color) {
BaseFont baseFont = null;
try {
baseFont = BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
} catch (DocumentException e) {
log.error("获取无效字体");
throw new RuntimeException(e);
} catch (IOException e) {
log.error("无法读取字体文件");
throw new RuntimeException(e);
}
return createFont(baseFont, fontSize, style, color);
}
/**
* 创建字体 {@link Font}
* @param baseFont 字体 {@link BaseFont}
* @param fontSize 字体大小
* @param style 字体风格 {@link Font}
* @param color 字体颜色 {@link BaseColor}
* @return {@link Font}
*/
public Font createFont(BaseFont baseFont, float fontSize, int style, BaseColor color) {
return new Font(baseFont, fontSize, style, color);
}
//------------------------------------其它------------------------------------
/**
* 关闭文档。<br>
* 将所有内容写入正文后,您必须关闭正文。在那之后,任何东西都不能再写入身体了。
*/
public void close() {
if (!close) {
open = false;
close = true;
}
for (DocListener listener : listeners) {
listener.close();
}
// 判断导出 PDF 是否带水印
if (watermark != null) {
createPdfStamper(destFilePath, getTargetFilePath());
setWatermark(watermark, 0.8f, 16, 7, 1, 45);
}
if (stamp != null) {
createPdfStamper(destFilePath, getTargetFilePath());
setImageWatermark();
}
closePdfStamper();
}
/**
* 创建 {@link PdfReader} 和 {@link PdfStamper}
* @param sourceFilePath 需要添加水印的 PDF 源文件路径
* @param targetFilePath 添加水印后的 PDF 文件路径
*/
private void createPdfStamper(String sourceFilePath, String targetFilePath) {
try {
if (reader == null || stamper == null) {
reader = new PdfReader(sourceFilePath);
stamper = new PdfStamper(reader, new FileOutputStream(targetFilePath));
}
} catch (DocumentException e) {
log.error("实例化阅读器失败!错误信息为: ", e);
throw new RuntimeException(e);
} catch (FileNotFoundException e) {
log.error("实例化阅读器失败!错误信息为: ", e);
throw new RuntimeException(e);
} catch (IOException e) {
log.error("实例化阅读器失败!错误信息为: ", e);
throw new RuntimeException(e);
}
}
/**
* 关闭 {@link PdfReader} 和 {@link PdfStamper}
*/
private void closePdfStamper() {
if (stamper != null) {
try {
stamper.close();
} catch (DocumentException e) {
log.error("关闭阅读器失败!错误信息为: ", e);
e.printStackTrace();
} catch (IOException e) {
log.error("关闭阅读器失败!错误信息为: ", e);
e.printStackTrace();
}
}
if (reader != null) {
reader.close();
}
}
//------------------------------------私有方法------------------------------------
/**
* 获取 PDF 写入流
* @param destFilePath PDF 存放路径
* @return PDF 写入流 {@link OutputStream}
*/
private OutputStream getPdfOutputStream(String destFilePath) {
try {
return Files.newOutputStream(Paths.get(destFilePath));
} catch (IOException e) {
log.error("PDF文件存在但是是目录而不是常规文件,不存在但无法创建,或者由于任何其他原因无法打开");
throw new RuntimeException(e);
}
}
/**
* 根据标题列表计算列表列数
* @param header 标题列表
* @return 列表列数
*/
private int calculateColumnNumber(List<String> header) {
int numColumns = 0;
for (String s : header) {
numColumns += calculateColumnNumber(s);
}
return numColumns;
}
/**
* 根据标题计算列表列数
* @param header 标题列表
* @return 列表列数
*/
private int calculateColumnNumber(String header) {
int numColumns = header.length();
if (!isChinese(header)) {
numColumns = numColumns / 2 + (numColumns % 2 != 0 ? 1 : 0);
}
return numColumns;
}
/**
* 字符串是否是中文
* @param str 字符串
* @return 是否是中文
*/
private boolean isChinese(String str) {
Pattern p = Pattern.compile("[\u4e00-\u9fa5]");
Matcher m = p.matcher(str);
return m.find();
}
/**
* 获取导出带水印的 PDF 文件路径
* @return 带水印的 PDF 文件路径
*/
private String getTargetFilePath() {
int i = destFilePath.lastIndexOf(".");
String suffix = destFilePath.substring(i);
String filePath = destFilePath.substring(0, i);
destFilePath = filePath + "(水印)" + suffix;
return destFilePath;
}
}