基于freemarker生成pdf

young 1,213 2021-10-17

环境准备

开发环境


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'></style>
</head>