目录

前言

依赖导入

模板文件+字体文件

 生成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即可,放置位置如下:

Java生成pdf流推送给浏览器 java将pdf流转为图片_ide

其中 SIMSUN.TTC 为字体文件,来自于windows系统下 C:\WINDOWS\Fonts 文件夹里面的宋体

Java生成pdf流推送给浏览器 java将pdf流转为图片_ci_02

 由于后来需求调整,模板直接定死了而不是随数据扩展,因此模板中大部分都是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;
    }
}

最后图片如下:

Java生成pdf流推送给浏览器 java将pdf流转为图片_ci_03