java Freemarker+HTML生成PDF证书
需要生成的证书是这样的:
1.需要用到的maven包
<!--证书生成开始-->
<!-- https://mvnrepository.com/artifact/com.itextpdf/itextpdf -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.itextpdf.tool/xmlworker -->
<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.19</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.xhtmlrenderer/flying-saucer-pdf -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.xhtmlrenderer/flying-saucer-pdf-itext5 -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-itext5</artifactId>
<version>9.1.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
<!--证书生成结束-->
2.pom.xml中写入静态文件路径
静态文件是放在 下列路径中
由于我的需要生成的PDF文件中的字体用到了楷体、黑体等字体,所以需要将这些文件引入到程序中
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.html</include>
<include>**/*.ttf</include>
<include>**/*.TTF</include>
<include>**/*.ttc</include>
</includes>
<!--是否替换资源中的属性-->
<filtering>false</filtering>
</resource>
</resources>
</build>
3.生成文件工具类
本地测试时,我文件放的位置是在D:/netplan/upload/目录下
测试环境和生产环境下,我文件放的位置是/opt/upload/下
你的html文件必须要放在你指定的路径 D:/netplan/upload/templateHtml/下
import com.itextpdf.text.pdf.BaseFont;
import com.lowagie.text.DocumentException;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import gc.cnnvd.framework.util.UUIDUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.CharEncoding;
import org.xhtmlrenderer.layout.SharedContext;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
/**
* Html生成PDF文件
*/
@Slf4j
public class HtmlToPDFUtils {
/**
* 模板存放位置 /opt/upload/templateHtml/
*/
private static final String TEMPLATE_HTML_PATH = "D:/netplan/upload/templateHtml/";
/**
* 技术支撑单位 certificate.html
*/
private static final String HTML_NAME = "compatibilityCertificate.html";
/**
* 临时HTML文件存放位置 /opt/upload/tempHtml/yyyy-MM-dd/时间戳.html
*/
private static final String TEMP_HTML_PATH = "D:/netplan/upload/templateHtml/";
/**
* 黑体
*/
private static final String SIMHEI = "fonts/simhei.ttf";
/**
* 楷体
*/
private static final String SIMKAI = "fonts/simkai.ttf";
/**
* 隶书
*/
private static final String SIMLI = "fonts/SIMLI.TTF";
/**
* 宋体
*/
private static final String SIMSUN = "fonts/simsun.ttc";
/**
* 渲染HTML文件并生成PDF文件
*
* @param params 传递到模板的参数
* @param targetFile 目标文件的地址
*/
public static void createPdfFile(Map<String, Object> params, String targetFile) {
Template template = null;
String renderResult = "";
//创建fm的配置
Configuration config = new Configuration();
//指定默认编码格式
config.setDefaultEncoding(CharEncoding.UTF_8);
try {
config.setDirectoryForTemplateLoading(new File(TEMPLATE_HTML_PATH));
template = config.getTemplate(HTML_NAME, CharEncoding.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
String outTempHtmlFilePath = TEMP_HTML_PATH + LocalDate.now().toString() + "/" + System.currentTimeMillis() + ".html";
File writeFile = new File(outTempHtmlFilePath);
File fileParent = writeFile.getParentFile();
if (!fileParent.exists()) {
fileParent.mkdirs();
}
try {
writeFile.createNewFile();
Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(writeFile), CharEncoding.UTF_8));
template.process(params, writer);
FileInputStream inputStream = new FileInputStream(outTempHtmlFilePath);
int length = inputStream.available();
byte bytes[] = new byte[length];
inputStream.read(bytes);
inputStream.close();
renderResult = new String(bytes, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
} catch (TemplateException e) {
e.printStackTrace();
}
try {
createPdf(renderResult, targetFile);
} catch (IOException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
}
}
public static void createPdf(String content, String dest) throws IOException, com.lowagie.text.DocumentException {
log.info("-----------------------content-----------------------");
log.info(content);
log.info("-----------------------content-----------------------");
ITextRenderer renderer = new ITextRenderer();
SharedContext sharedContext = renderer.getSharedContext();
// 解决base64图片支持问题
sharedContext.setReplacedElementFactory(new Base64ImgReplacedElementFactory());
sharedContext.getTextRenderer().setSmoothingThreshold(0);
ITextFontResolver fontResolver = renderer.getFontResolver();
fontResolver.addFont(SIMHEI, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
fontResolver.addFont(SIMLI, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
fontResolver.addFont(SIMKAI, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
fontResolver.addFont(SIMSUN, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
// 解析html生成pdf
renderer.setDocumentFromString(content);
//解决图片相对路径的问题
renderer.layout();
renderer.createPDF(new FileOutputStream(dest));
}
public static void main(String[] args) {
String outFilePath = "D:/netplan/" + UUIDUtil.getUuid() + ".pdf";
Map<String, Object> data = new HashMap();
data.put("certificateCode", "CNNVD-XX-XXXXXX");
data.put("companyName", "北京XXXXXX有限公司");
data.put("registerAddress", "北京市海淀区XXXXXXXXXXX");
data.put("creditCode", "XXXXXXXXXXXXXXXX");
data.put("productName", "测试产品名称");
data.put("productVersion", "V1.03");
data.put("productModel", "SKPI2000XXX");
data.put("reviewTime", "2021年06月25日");
data.put("expiryTime", "2022年06月25日");
data.put("currentTime", "2021年08月06日");
createPdfFile(data, outFilePath);
}
}
图片工具类
import com.lowagie.text.BadElementException;
import com.lowagie.text.Image;
import com.lowagie.text.pdf.codec.Base64;
import org.w3c.dom.Element;
import org.xhtmlrenderer.extend.FSImage;
import org.xhtmlrenderer.extend.ReplacedElement;
import org.xhtmlrenderer.extend.ReplacedElementFactory;
import org.xhtmlrenderer.extend.UserAgentCallback;
import org.xhtmlrenderer.layout.LayoutContext;
import org.xhtmlrenderer.pdf.ITextFSImage;
import org.xhtmlrenderer.pdf.ITextImageElement;
import org.xhtmlrenderer.render.BlockBox;
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
import java.io.IOException;
/**
* * 图片base64支持,把图片转换为itext自己的图片对象
* * @author Administrator
* *
*
*/
public class Base64ImgReplacedElementFactory implements ReplacedElementFactory {
/**
* 实现createReplacedElement 替换html中的Img标签
*
* @param c 上下文
* @param box 盒子
* @param uac 回调
* @param cssWidth css宽
* @param cssHeight css高
* @return ReplacedElement
*/
@Override
public ReplacedElement createReplacedElement(LayoutContext c, BlockBox box, UserAgentCallback uac,
int cssWidth, int cssHeight) {
Element e = box.getElement();
if (e == null) {
return null;
}
String nodeName = e.getNodeName();
// 找到img标签
if (nodeName.equals("img")) {
String attribute = e.getAttribute("src");
FSImage fsImage;
try {
// 生成itext图像
fsImage = buildImage(attribute, uac);
} catch (BadElementException e1) {
fsImage = null;
} catch (IOException e1) {
fsImage = null;
}
if (fsImage != null) {
// 对图像进行缩放
if (cssWidth != -1 || cssHeight != -1) {
fsImage.scale(cssWidth, cssHeight);
}
return new ITextImageElement(fsImage);
}
}
return null;
}
/**
* 将base64编码解码并生成itext图像
*
* @param srcAttr 属性
* @param uac 回调
* @return FSImage
* @throws IOException io异常
* @throws BadElementException BadElementException
*/
protected FSImage buildImage(String srcAttr, UserAgentCallback uac) throws IOException,
BadElementException {
FSImage fsImage;
if (srcAttr.startsWith("data:image/")) {
String b64encoded = srcAttr.substring(srcAttr.indexOf("base64,") + "base64,".length(),
srcAttr.length());
// 解码
byte[] decodedBytes = Base64.decode(b64encoded);
fsImage = new ITextFSImage(Image.getInstance(decodedBytes));
} else {
fsImage = uac.getImageResource(srcAttr).getImage();
}
return fsImage;
}
/**
* 实现remove
*
* @param e 元素
*/
@Override
public void remove(Element e) {
}
/**
* 实现reset
*/
@Override
public void reset() {
}
/**
* 实现setFormSubmissionListener
*
* @param formsubmissionlistener 监听
*/
@Override
public void setFormSubmissionListener(FormSubmissionListener formsubmissionlistener) {
}
}
4.html证书模板
其中图片需要使用到的是绝对路径
当前图片展示的是本地的路径 在服务器上运行的话需换成服务器上的路径
还有需要注意生成文件的大小 在HTML文件的@page
中,当前我的纸张大小为A4纸
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>兼容性服务证书</title>
<style>
@page {
size: a4
}
* {
margin: 0;
padding: 0;
list-style: none;
}
.main {
background-color: #fffbc6;
width: 700px;
height: 1000px;
position: relative;
}
.main .mainin {
margin: auto;
background-color: #fffbc6;
background: url("file:///D:/netplan/images/border.png") no-repeat;
background-size: 640px 940px;
width: 640px;
height: 940px;
text-align: center;
font-family: 'NSimSun';
position: relative;
top: 30px;
}
.main .mainin img {
margin: 70px 0 40px 0;
}
.main .mainin .p1 {
font: normal small-caps normal 38px/45px 'LiSu';
}
.main .mainin .p2 {
font: normal small-caps normal 50px/55px 'LiSu';
}
.main .mainin .certificate-code {
letter-spacing: 5px;
font: normal small-caps normal 18px/80px 'SimHei';
}
.main .mainin .company-name {
letter-spacing: 5px;
font: normal small-caps normal 28px/40px 'SimHei';
}
.main .mainin .company-info {
margin-top: 10px;
letter-spacing: 2px;
font-size: 18px;
}
.main .mainin .company-info span::after {
content: '';
display: inline-block;
width: 100%;
}
.main .mainin .company-info span {
font: normal small-caps normal 16px/40px 'SimHei';
}
.main .mainin .peoduct-info {
letter-spacing: 5px;
margin-top: 40px;
font: normal small-caps normal 28px/40px 'SimHei';
}
.main .mainin .peoduct-version {
letter-spacing: 5px;
font: normal small-caps normal 28px/40px 'SimHei';
}
.main .mainin .instruction {
font: normal small-caps normal 20px/55px 'SimHei';
}
.main .mainin .date-info {
font-family: 'NSimSun';
margin-top: 40px;
}
.main .mainin .date-info {
font: normal small-caps normal 18px/35px 'SimHei';
}
.main .mainin .date-info span {
letter-spacing: 10px;
}
.main .mainin .approve-info {
margin-top: 40px;
}
.main .mainin .approve-info div {
text-align: right;
margin-right: 100px;
font: normal small-caps normal 18px/35px 'SimHei';
}
</style>
</head>
<body>
<div class="main">
<div class="mainin">
<img src="file:///D:/netplan/images/certificateLogo.png" width="150" height="150" />
<p class="p1">国家信息安全漏洞库</p>
<p class="p2">兼容性资质证书</p>
<p class="certificate-code"><span>证书号:</span>${certificateCode}</p>
<p class="company-name">${companyName}</p>
<div class="company-info">
(${registerAddress}<br />
统一社会信用代码:${creditCode})
</div>
<p class="peoduct-info">${productName}</p>
<p class="peoduct-version">(${productVersion}/ ${productModel})</p>
<p class="instruction">此证书仅适用于该产品/系统的上述版本和型号</p>
<div class="date-info">
<span>签发日期</span>:${reviewTime}<br />
<span>有效期至</span>:${expiryTime}
</div>
<div class="approve-info">
<div>${currentTime}</div>
</div>
</div>
</div>
</body>
</html>