目录
前言
依赖导入
模板文件+字体文件
生成pdf
pdf转图片
前言
上半年做过一个生成pdf文件的需求,网上资料太杂,为了方便后续查看本文做一个记录,大概需求如下:查询某个儿童的疫苗接种数据并生成pdf文件,但是由于微信公众号端无法下载pdf,因此后端需要将pdf转换为图片方便展示和保存。主要思路就是使用 Freemarker 去借助 html 模板生成 pdf 文件,然后通过 pdfbox 将pdf转为图片并返回
依赖导入
<!-- freemarker 读取html模板文件 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<!-- xml 将html模板文件转换成pdf -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.0.9</version>
</dependency>
<!-- pdf转图片 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.12</version>
</dependency>
模板文件+字体文件
其实就是html文件,把后缀改为.ftl即可,放置位置如下:
其中 SIMSUN.TTC 为字体文件,来自于windows系统下 C:\WINDOWS\Fonts 文件夹里面的宋体
由于后来需求调整,模板直接定死了而不是随数据扩展,因此模板中大部分都是html语法,若需要处理数组之类的,可自行搜索ftl语法,模板内容如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title></title>
<style>
*{
margin: 100;
padding: 100;
box-sizing: border-box;
}
body{
font-family: SimSun;
width: 600px;
}
section{
display:block;
margin: 20px 10px;
}
tr{
margin: 100;
}
.title{
text-align: center;
}
.preface p{
line-height: 30px;
}
.img{
opacity: 0.4;
}
.preface p.content{
text-indent: 2em;
}
section > table{
table-layout: fixed;
width: 100%;
margin: 20px 0px;
text-align:center;
word-wrap:break-word;
}
section table td{
padding:5px 0px;
}
.ceshi{
width: 700px;
}
.box2{
position: relative;
float: right;
left: 450px;
top: 130px;
}
.box2 img{
position: absolute;
left: 80px;
top: -88px;
}
.img{
position: absolute;
left: 600px;
top: 0px;
}
</style>
</head>
<body>
<!-- 标题 start -->
<div class="ceshi">
<section class="title">
<h2>国家免疫规划疫苗预防接种查验单</h2>
</section>
<!-- 标题 end -->
<!-- 前言 start -->
<img src="${qrcode}" class="img" width="100" height="100"/>
<table class="preface" style="width: 700px; border-spacing:0px 15px;table-layout: fixed;" cellspacing="0" cellpadding="0">
<h4>一、基本信息</h4 >
<tr>
<td>姓名: ${name!''}</td>
<td>性别:${sex!''}</td>
<td>出生日期:${birthTime!''}</td>
</tr>
<tr>
<td>家长姓名:${parentName!''}</td>
<td>联系方式:${parentPhone!''}</td>
<td>居委会:${neighborhoodCommittee!''}</td>
</tr>
<tr>
<td style="white-space: nowrap;">户籍地址:${registeredResidenceAddress!''}</td>
</tr>
<tr>
<td style="white-space: nowrap;">现住址:${residentialAddress!''}</td>
</tr>
<tr>
<td style="white-space: nowrap;">托幼机构(学校)名称:${schoolName!''}</td>
</tr>
<tr>
</tr>
</table>
<!-- 前言 end -->
<!-- 明细 start -->
<section class="detail">
<h4>二、接种情况</h4>
<table border="1" style="width: 100%;" cellspacing="0" cellpadding="0">
<tr>
<td style="width: 10%;">疫苗名称</td>
<td style="width: 12%;">剂次</td>
<td style="width: 23%;">接种日期</td>
<td >疫苗名称</td>
<td style="width: 12%;">剂次</td>
<td style="width: 25%;">接种日期</td>
</tr>
<tr>
<td rowspan="3">乙肝疫苗(HepB)</td>
<td>第1剂次</td>
<td>${time1}</td>
<td>白破疫苗(DT)</td>
<td>第1剂次</td>
<td>${time15}</td>
</tr>
<tr>
<td>第2剂次</td>
<td>${time2}</td>
<td rowspan="2">A群流脑疫苗(MenA)</td>
<td>第1剂次</td>
<td>${time16}</td>
</tr>
<tr>
<td>第3剂次</td>
<td>${time3}</td>
<td>第2剂次</td>
<td>${time17}</td>
</tr>
<tr>
<td>卡介疫苗(BCG)</td>
<td>第1剂次</td>
<td>${time4}</td>
<td rowspan="2">A+C群流脑疫苗(MenAC)</td>
<td>第1剂次</td>
<td>${time18}</td>
</tr>
<tr>
<td rowspan="4">脊灰疫苗(OPv)</td>
<td>第1剂次</td>
<td>${time5}</td>
<td>第2剂次</td>
<td>${time19}</td>
</tr>
<tr>
<td>第2剂次</td>
<td>${time6}</td>
<td rowspan="4">乙脑疫苗(JEV)</td>
<td>第1剂次</td>
<td>${time20}</td>
</tr>
<tr>
<td>第3剂次</td>
<td>${time7}</td>
<td>第2剂次</td>
<td>${time21}</td>
</tr>
<tr>
<td>第4剂次</td>
<td>${time8}</td>
<td>第3剂次</td>
<td>${time22}</td>
</tr>
<tr>
<td rowspan="4">百白破疫苗(DPT)</td>
<td>第1剂次</td>
<td>${time9}</td>
<td>第4剂次</td>
<td>${time23}</td>
</tr>
<tr>
<td>第2剂次</td>
<td>${time10}</td>
<td rowspan="2">甲肝疫苗(HepA)</td>
<td>第1剂次</td>
<td>${time24}</td>
</tr>
<tr>
<td>第3剂次</td>
<td>${time11}</td>
<td>第2剂次</td>
<td>${time25}</td>
</tr>
<tr>
<td>第4剂次</td>
<td>${time12}</td>
<td rowspan="2">水痘(Var)</td>
<td>第1剂次</td>
<td>${time26}</td>
</tr>
<tr>
<td rowspan="2">含麻疹类疫苗(MCV)*</td>
<td>第1剂次</td>
<td>${time13}</td>
<td>第2剂次</td>
<td>${time27}</td>
</tr>
<tr>
<td>第2剂次</td>
<td>${time14}</td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</section>
<section class="detail">
<h4>三、查验结果</h4>
<p>已完成该年龄段国家免疫规划疫苗接种</p >
</section>
<div class="box2">
<p>预防接种单位(盖章): ______________________
<img src="${dzz}" class="img" width="200" height="200"/>
<p style="position: absolute; left: 140px;">${now}</p >
</p >
</div>
</div>
</body>
</html>
模板里的图片及二维码都是通过base64的方式直接填充展示
生成pdf
工具类如下,可直接使用
public class PDFTemplateUtil {
/**
* 通过模板导出pdf文件
* @param data 数据
* @param templateFileName 模板文件名
* @throws Exception
*/
public static ByteArrayOutputStream createPDF(Map<String,Object> data, String templateFileName) throws Exception {
// 创建一个FreeMarker实例, 负责管理FreeMarker模板的Configuration实例
Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
// 指定FreeMarker模板文件的位置
cfg.setClassForTemplateLoading(PDFTemplateUtil.class, File.separator + "templates");
ITextRenderer renderer = new ITextRenderer();
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
// 设置 css中 的字体样式(暂时仅支持宋体和黑体) 必须,不然中文不显示
renderer.getFontResolver().addFont(File.separator + "static" + File.separator + "font" + File.separator + "SIMSUN.TTC", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
// 设置模板的编码格式
cfg.setEncoding(Locale.CHINA, "UTF-8");
// 获取模板文件
Template template = cfg.getTemplate(templateFileName, "UTF-8");
StringWriter writer = new StringWriter();
// 将数据输出到html中
template.process(data, writer);
writer.flush();
String html = writer.toString();
// 把html代码传入渲染器中
renderer.setDocumentFromString(html);
renderer.layout();
renderer.createPDF(out, false);
renderer.finishPDF();
out.flush();
return out;
}
}
}
上层调用:
private ByteArrayOutputStream createVaccinationPDF(String childId, String language, String qrcodeUrl) {
ByteArrayOutputStream pdfOs = null;
try {
// 儿童基本信息
String name, sex, birthTime, parentName, parentPhone, registeredResidenceAddress, residentialAddress,
schoolName = null, templateFileName = "接种报告模板zh.ftl";
ChildrenInfo childrenInfo = childrenInfoService.getById(childId);
if (null == childrenInfo) {
// 此处做个兼容,若当前儿童未关联系统,则需要去总包库里查询
VaccinatorfInfo vaccinatorfInfo = vaccinatorfInfoService.getByVaccinationId(childId);
if (null == vaccinatorfInfo) {
throw new AsocoCommonException("当前儿童无对应信息");
} else {
name = vaccinatorfInfo.getName();
sex = vaccinatorfInfo.getSexCode();
birthTime = sdf.format(vaccinatorfInfo.getBirthTime());
parentName = StringUtils.isNotEmpty(vaccinatorfInfo.getFatherName()) ? vaccinatorfInfo.getFatherName() : childrenInfo.getMotherName();
parentPhone = StringUtils.isNotEmpty(vaccinatorfInfo.getFatherName()) ? vaccinatorfInfo.getFatherTel() : childrenInfo.getMotherTel();
registeredResidenceAddress = vaccinatorfInfo.getHouseholdAddr();
residentialAddress = vaccinatorfInfo.getCurrAddr();
}
} else {
name = childrenInfo.getName();
sex = childrenInfo.getSex();
birthTime = sdf.format(childrenInfo.getBirthTime());
parentName = StringUtils.isNotEmpty(childrenInfo.getFatherName()) ? childrenInfo.getFatherName() : childrenInfo.getMotherName();
parentPhone = StringUtils.isNotEmpty(childrenInfo.getFatherName()) ? childrenInfo.getFatherTel() : childrenInfo.getMotherTel();
registeredResidenceAddress = childrenInfo.getRegisteredResidenceAddress();
residentialAddress = childrenInfo.getResidentialAddress();
schoolName = childrenInfo.getSchoolName();
}
Map<String, Object> data = new HashMap<>();
if (StringConstant.LANGUAGE_ENGLISH.equals(language)) {
// 暂时不管,日后中文转英文可使用百度api,每月免费200w字符
templateFileName = "接种报告模板en.ftl";
}
data.put("name", name);
data.put("sex", sex);
data.put("birthTime", birthTime);
data.put("parentName", parentName);
data.put("parentPhone", parentPhone);
data.put("registeredResidenceAddress", registeredResidenceAddress);
data.put("residentialAddress", residentialAddress);
data.put("schoolName", schoolName);
// 电子章
data.put("dzz", "data:image/png;base64," + transformBase64("static" + File.separator + "images" + File.separator + "dzz.png"));
// 二维码
if (StringUtils.isNotEmpty(qrcodeUrl)) {
BufferedImage bufferedImage = QRCodeUtil.getBufferedImage(qrcodeUrl);
String base = transformBase64(bufferedImage);
data.put("qrcode", base);
}
// 查询当前儿童接种记录
List<VaccinationVo> result = listChildVaccination(childId, null);
// 构建需要展示的结果
constructNeedVaccination(result, data);
// data.put("detailList", pdfVaccinationVos);
data.put("now", new SimpleDateFormat(StringConstant.DATE_FORMAT_5).format(new Date()));
pdfOs = PDFTemplateUtil.createPDF(data, templateFileName);
} catch (Exception e) {
log.error("生成接种报告pdf异常!", e);
}
return pdfOs;
}
该接口返回的是pdf的字节数组输出流,如果你只是想导出pdf,那么直接将流输出到 HttpServletResponse 即可,大概方法如下:
public void generateVaccinationReportPdf(HttpServletResponse response, String childId, String language) {
try (OutputStream out = response.getOutputStream()) {
String childName = getChildNameById(childId);
String picFileName = fileService.packageFileName(childName + "接种报告.png");
String pdfFileName = fileService.packageFileName(childName + "接种报告.pdf");
ByteArrayOutputStream pdfOs = createVaccinationPDF(childId, language, packageQrcodeUrl(picFileName));
convertPdfToPicAndUpload(pdfOs, picFileName);
// 设置响应消息头,告诉浏览器当前响应是一个下载文件
response.setContentType("application/x-msdownload");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(pdfFileName, "UTF-8"));
pdfOs.writeTo(out);
} catch (Exception e) {
log.error("返回pdf异常:{}", e.getMessage());
}
}
pdf转图片
方法如下:
private List<String> convertPdfToPicAndUpload(ByteArrayOutputStream pdfOs, String fileName) {
List<String> result = new ArrayList<>();
try {
// 将pdf转成图片
ByteArrayInputStream swapStream = new ByteArrayInputStream(pdfOs.toByteArray());
PDDocument pdDocument = PDDocument.load(swapStream);
PDFRenderer renderer = new PDFRenderer(pdDocument);
int pages = pdDocument.getNumberOfPages();
List<BufferedImage> picList = new ArrayList<>();
for (int i = 0; i < pages; i++) {
// dpi越大转换后越清晰 相对转换速度越慢 105相对合适
BufferedImage image = renderer.renderImageWithDPI(i, 105);
picList.add(image);
result.add(transformBase64(image));
}
pdDocument.close();
// 上传图片时需要合并处理
BufferedImage newPic = ImageUtil.mergePic(picList);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(newPic, "png", bos);
fileService.upload(fileName, bos);
bos.flush();
bos.close();
} catch (Exception e) {
log.error("将pdf转成图片并上传时发生错误:{}", e.getMessage());
}
return result;
}
图片合并工具类:
public class ImageUtil {
/**
* 将宽度相同的图片,竖向追加在一起 ##注意:宽度必须相同
* @param picList 文件流数组
* @return 新图片的文件流
*/
public static BufferedImage mergePic(List<BufferedImage> picList) {// 纵向处理图片
BufferedImage result = null;
if (!CollectionUtils.isEmpty(picList) && picList.size() > 1) {
try {
// 总高度
int height = 0,
// 总宽度
width = 0,
// 临时的高度 , 或保存偏移高度
offsetHeight = 0,
// 临时的高度,主要保存每个高度
tmpHeight = 0,
// 图片的数量
picNum = picList.size();
// 保存每个文件的高度
int[] heightArray = new int[picNum];
// 保存图片流
BufferedImage buffer = null;
// 保存所有的图片的RGB
List<int[]> imgRgb = new ArrayList<>();
// 保存一张图片中的RGB数据
int[] tmpImgRgb;
for (int i = 0; i < picNum; i++) {
buffer = picList.get(i);
// 图片高度
heightArray[i] = offsetHeight = buffer.getHeight();
if (i == 0) {
// 图片宽度
width = buffer.getWidth();
}
// 获取总高度
height += offsetHeight;
// 从图片中读取RGB
tmpImgRgb = new int[width * offsetHeight];
tmpImgRgb = buffer.getRGB(0, 0, width, offsetHeight, tmpImgRgb, 0, width);
imgRgb.add(tmpImgRgb);
}
// 设置偏移高度为0
offsetHeight = 0;
// 生成新图片
result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int i = 0; i < picNum; i++) {
tmpHeight = heightArray[i];
if (i != 0) {
// 计算偏移高度
offsetHeight += tmpHeight;
}
// 写入流中
result.setRGB(0, offsetHeight, width, tmpHeight, imgRgb.get(i), 0, width);
}
} catch (Exception e) {
log.error("合并图片异常:{}", e.getMessage());
}
} else if (!CollectionUtils.isEmpty(picList) && picList.size() == 1) {
result = picList.get(0);
}
return result;
}
}
最后图片如下: