环境准备
开发环境
java8
,SpringBoot 2.1.4
,字符集GBK
字体
宋体–simsun.ttf
pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>7.0.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>io</artifactId>
<version>7.0.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>forms</artifactId>
<version>7.0.3</version>
</dependency>
<!-- 解决中文字体问题 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>font-asian</artifactId>
<version>7.0.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.13</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.5</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-itext5</artifactId>
<version>9.1.5</version>
</dependency>
模板生成PDF
将模板转换为html
从文件读取模板
/**
* 初始化freemarker配置
* templateRoot:模板文件根目录
*/
Configuration freemarkerCfg = initFreemarkerCfg(templateRoot);
/**
* 将模板转换为HTML字符串
*/
String content = freeMarkerRender(data, freemarkerCfg, htmlTemplate, charSet);
private static Configuration initFreemarkerCfg(String templateRoot) {
Configuration freemarkerCfg = new Configuration();
try {
freemarkerCfg.setDirectoryForTemplateLoading(new File(templateRoot));
} catch (IOException e) {
log.error("模板根路径获取失败!" + templateRoot, e);
throw new RuntimeException("模板根路径获取失败!" + templateRoot,e);
}
return freemarkerCfg;
}
/**
* data 需要注入模板的数据
* freemarkerCfg freemarker配置
* htmlTmp 模板名称
* charSet 字符集 linux下使用UTF-8,windows下使用GBK,否则会出现中文乱码,模板文件的文件编码和声明编码同样需要保持一致
*/
private static String freeMarkerRender(Map<String, Object> data, Configuration freemarkerCfg, String htmlTmp,String charSet) {
try (Writer out = new StringWriter();) {
Template template = freemarkerCfg.getTemplate(htmlTmp);
template.setEncoding(charSet);
template.process(data, out);
out.flush();
return out.toString();
} catch (Exception e) {
log.error("HTML加载数据失败!", e);
throw new RuntimeException("HTML加载数据失败!", e);
}
}
从流读取模板
/**
* data 需要注入模板的数据
* fileName 文件名称
* inputStream 模板文件流
* charSet 字符集 linux下使用UTF-8,windows下使用GBK,否则会出现中文乱码,模板文件的文件编码和声明编码同样需要保持一致
*/
private static String freeMarkerRender(Map<String, Object> data, String fileName, InputStream inputStream,
String charSet) {
try (Writer out = new StringWriter();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);) {
Configuration configuration = new Configuration();
Template template = new Template(fileName, inputStreamReader, configuration);
template.setEncoding(charSet);
template.process(data, out);
out.flush();
return out.toString();
} catch (Exception e) {
log.debug("HTML加载数据失败!", e);
throw new RuleException(ErrCodeFile.CO_HTML_TEMPLATE_CONVERT_ERROR);
}
}
生成PDF
/**
* htmlContent 通过freemarker生成的html
* fontPath 字体文件路径
* ByteArrayOutputStream pdf文件流
*/
private static ByteArrayOutputStream htmlToPdf(String htmlContent, String fontPath) {
try {
ByteArrayOutputStream output = new ByteArrayOutputStream();
ITextRenderer render = new ITextRenderer();
ITextFontResolver fontResolver = render.getFontResolver();
fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
render.setDocumentFromString(htmlContent);
render.getSharedContext().setBaseURL(BASE_URL);
render.layout();
render.createPDF(output);
return output;
} catch (Exception e) {
log.debug("html转换pdf失败!", e);
throw new RuntimeException("html转换pdf失败!", e);
}
}
添加水印和页码
从文件获取水印
/**
* outputStream 生成的pdf流
* waterMarkPath 水印文件路径
* fontPath 字体路径
* OutputStream pdf文件流
*/
private static OutputStream addWaterImage(ByteArrayOutputStream outputStream, String waterMarkPath,
String fontPath) {
BaseFont baseFont = createFont(fontPath);
try (InputStream input = new ByteArrayInputStream(outputStream.toByteArray());) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
PdfReader reader = new PdfReader(input);
PdfStamper stamp = new PdfStamper(reader, output);
PdfContentByte contentByte = null;
int n = reader.getNumberOfPages();
Image logo = null;
if(StringUtils.isNotBlank(waterMarkPath)){
logo = Image.getInstance(waterMarkPath);
}
for (int i = 1; i <= n; i++) {
contentByte = stamp.getUnderContent(i);
Rectangle rectangle = reader.getPageSize(i);
float width = rectangle.getWidth();
float height = rectangle.getHeight();
if(logo != null){
logo.setAbsolutePosition(width / 2 - logo.getWidth() / 2, height / 2);
contentByte.addImage(logo);
contentByte.saveState();
}
String text = "第 " + i + " 页 /共 " + n + " 页";
contentByte.beginText();
contentByte.setFontAndSize(baseFont, 12);
contentByte.showTextAligned(Element.ALIGN_CENTER, text, (width / 2) - 6, 15, 0);
contentByte.endText();
}
reader.close();
stamp.close();
return output;
} catch (Exception e) {
log.debug("添加水印和页码失败," + waterMarkPath, e);
throw new RuntimeException("添加水印和页码失败," + waterMarkPath, e);
}
}
private static BaseFont createFont(String fontPath) {
try {
return BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
} catch (Exception e) {
log.debug("字体读取失败," + fontPath, e);
throw new RuntimeException("字体读取失败," + fontPath, e);
}
}
从流获取水印
/**
* outputStream 生成的pdf流
* waterMarkPath 水印流
* fontPath 字体路径
* OutputStream pdf文件流
*/
private static OutputStream addWaterImage(ByteArrayOutputStream outputStream, InputStream waterMarkPath,
String fontPath) {
BaseFont baseFont = createFont(fontPath);
try (InputStream input = new ByteArrayInputStream(outputStream.toByteArray());) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
PdfReader reader = new PdfReader(input);
PdfStamper stamp = new PdfStamper(reader, output);
PdfContentByte contentByte = null;
int n = reader.getNumberOfPages();
Image logo = null;
if(waterMarkPath != null){
byte[] waterMarkBytes = IOUtils.toByteArray(inputStream);
logo = Image.getInstance(waterMarkBytes);
}
for (int i = 1; i <= n; i++) {
contentByte = stamp.getUnderContent(i);
Rectangle rectangle = reader.getPageSize(i);
float width = rectangle.getWidth();
float height = rectangle.getHeight();
if(logo != null){
logo.setAbsolutePosition(width / 2 - logo.getWidth() / 2, height / 2);
contentByte.addImage(logo);
contentByte.saveState();
}
String text = "第 " + i + " 页 /共 " + n + " 页";
contentByte.beginText();
contentByte.setFontAndSize(baseFont, 12);
contentByte.showTextAligned(Element.ALIGN_CENTER, text, (width / 2) - 6, 15, 0);
contentByte.endText();
}
reader.close();
stamp.close();
return output;
} catch (Exception e) {
log.debug("添加水印和页码失败," + waterMarkPath, e);
throw new RuntimeException("添加水印和页码失败," + waterMarkPath, e);
}
}
pdf加密码及权限设置
权限说明
权限 | 说明 |
---|---|
ALLOW_PRINTING | 文档允许打印 |
ALLOW_DEGRADED_PRINTING | 允许用户打印文档,但不提供allow_printing质量(128位加密) |
ALLOW_MODIFY_CONTENTS | 允许用户修改内容,例如 更改页面内容,或插入或删除页 |
ALLOW_ASSEMBLY | 允许用户插入、删除和旋转页面和添加书签。页面的内容不能更改,除非也授予allow_modify_contents权限,(128位加密) |
ALLOW_COPY | 允许用户复制或以其他方式从文档中提取文本和图形,包括使用辅助技术。例如屏幕阅读器或其他可访问设备 |
ALLOW_SCREENREADERS | 允许用户提取文本和图形以供易访问性设备使用,(128位加密) |
ALLOW_MODIFY_ANNOTATIONS | 允许用户添加或修改文本注释和交互式表单字段 |
ALLOW_FILL_IN | 允许用户填写表单字段,(128位加密) |
需要多个权限时,用|
拼接即可
无水印页码
在生成PDF时添加权限及密码
private static ByteArrayOutputStream htmlToPdf(String htmlContent, String fontPath, String password, String adminPassword) {
try {
ByteArrayOutputStream output = new ByteArrayOutputStream();
ITextRenderer render = new ITextRenderer();
ITextFontResolver fontResolver = render.getFontResolver();
setPDFEncryption(password, adminPassword, render);
fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
render.setDocumentFromString(htmlContent);
render.getSharedContext().setBaseURL(BASE_URL);
render.layout();
render.createPDF(output);
return output;
} catch (Exception e) {
log.debug("html转换pdf失败!", e);
throw new RuleException(ErrCodeFile.CO_HTML_TO_PDF_FAILED);
}
}
/** 用户权限,根据需求自己设置*/
private static final int PERMIT = PdfWriter.ALLOW_PRINTING;
/** pdf加密类型*/
private static final int ENCRYPTION_TYPE = PdfWriter.STANDARD_ENCRYPTION_128;
private static void setPDFEncryption(String password, String adminPassword,ITextRenderer render) {
PDFEncryption pdfEncryption = new PDFEncryption();
// 用户密码
pdfEncryption.setUserPassword(password.getBytes());
// 管理员密码
pdfEncryption.setOwnerPassword(adminPassword.getBytes());
// 用户权限
pdfEncryption.setAllowedPrivileges(PERMIT);
// 加密类型
pdfEncryption.setEncryptionType(ENCRYPTION_TYPE);
render.setPDFEncryption(pdfEncryption);
}
有水印页码
在生成PDF时无需添加权限及密码,在添加水印及页码时添加
private static OutputStream addWaterImage(ByteArrayOutputStream outputStream, InputStream waterMarkPath,
String fontPath, String password, String adminPassword) {
BaseFont baseFont = createFont(fontPath);
try (InputStream input = new ByteArrayInputStream(outputStream.toByteArray());) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
PdfReader reader = new PdfReader(input);
PdfStamper stamp = new PdfStamper(reader, output);
// 用户密码,管理员密码,权限,加密类型
stamp.setEncryption(password.getBytes(), adminPassword.getBytes(), PERMIT, ENCRYPTION_TYPE);
PdfContentByte contentByte = null;
int n = reader.getNumberOfPages();
Image logo = null;
if(waterMarkPath != null){
byte[] waterMarkBytes = IOUtils.toByteArray(inputStream);
logo = Image.getInstance(waterMarkBytes);
}
for (int i = 1; i <= n; i++) {
contentByte = stamp.getUnderContent(i);
Rectangle rectangle = reader.getPageSize(i);
float width = rectangle.getWidth();
float height = rectangle.getHeight();
if(logo != null){
logo.setAbsolutePosition(width / 2 - logo.getWidth() / 2, height / 2);
contentByte.addImage(logo);
contentByte.saveState();
}
String text = "第 " + i + " 页 /共 " + n + " 页";
contentByte.beginText();
contentByte.setFontAndSize(baseFont, 12);
contentByte.showTextAligned(Element.ALIGN_CENTER, text, (width / 2) - 6, 15, 0);
contentByte.endText();
}
reader.close();
stamp.close();
return output;
} catch (Exception e) {
log.debug("添加水印和页码失败," + waterMarkPath, e);
throw new RuntimeException("添加水印和页码失败," + waterMarkPath, e);
}
}
模板记录
1、字符集乱码问题
<meta http-equiv="Content-Type" content="text/html; charset=GBK"/>
(历史原因导致开发使用GBK字符集,UTF-8的情况暂时未知)charset应与Java代码中的传入的保持一致,同时ftl文件的字符集应与此保持一致,windows使用GBK,linux使用UTF-8,否则生成PDF乱码
2、img标签
<img src="${logoImage}" width="204"/>
img标签支持base64格式data:image/png;base64,
data:,文本数据
data:text/plain,文本数据
data:text/html,HTML代码
data:text/html;base64,base64编码的HTML代码
data:text/css,CSS代码
data:text/css;base64,base64编码的CSS代码
data:text/javascript,Javascript代码
data:text/javascript;base64,base64编码的Javascript代码
data:image/gif;base64,base64编码的gif图片数据
data:image/png;base64,base64编码的png图片数据
data:image/jpeg;base64,base64编码的jpeg图片数据
data:image/x-icon;base64,base64编码的icon图片数据
3、强制分页
<p style="margin: 0pt">
<div style="page-break-before: always; clear: both"/>
</p>
4、 head记录
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GBK"/>
<meta http-equiv="Content-Style-Type" content="text/css"/>
<title>xxxx</title>
<style type='text/css'>
body {
font-family: SimSun;
padding-top: 50px;
}
@page {
size: a4;
@top-center {
content: element(header);
}
@bottom-center {
content: element(footer);
}
}
div.header {
display: block;
/*text-align: center;*/
position: running(header);
width: 100%;
}
div.footer {
display: block;
text-align: center;
position: running(footer);
width: 100%;
}
.custom-page-start {
margin-top: 50px;
}
table {
border-collapse: collapse;
margin: 0 auto;
width: 100%;
}
td {
border: #000000 solid 0.75pt;
vertical-align: top;
padding: 5pt;
}
p {
line-height: 18pt;
margin: 0pt 0pt 4pt;
}
span {
font-size: 10pt;
}
@media print {
table {
page-break-after: auto
}
tr {
page-break-inside: avoid;
page-break-after: auto
}
td {
page-break-inside: avoid;
page-break-after: auto
}
thead {
display: table-header-group
}
tfoot {
display: table-footer-group
}
}
</style>
</head>