SpringBoot集成itext5,实现pdf生成
1、前言
在项目开发中有需要生成pdf文件的功能,常见的方案如下:
- html作为模板,(Thymeleaf、freemaker等模板引擎)渲染数据,最后生成pdf
- 使用pdf模板(使用如:Adobe Acrobat编辑器,创建一个带有表单字段的PDF),填充数据,最后生成pdf
- 使用pdf模板(使用占位符,比如${name}),用数据替换占位符,最后生成pdf。比较复杂,不容易实现,不建议。
如何得到pdf文件,主要有两种方法,一种是写代码生成,还有就是用pdf转换工具,比如wkhtmltopdf之类的。由于在项目中考虑到,使用html的方案,在修改模板时对用户不友好,所以采用了方案2:pdf模板+表单+填充数据。
工具使用itext5,因为itext7免费使用受到了某些限制,必须遵循 AGPL 开源协议,或购买商业许可证。还有其他免费的,如:Apache PDFBox,特点如下:
| 技术 | 特点 | 适用场景 |
|---|---|---|
| iText | 功能强大,支持复杂 PDF 操作 | 复杂 PDF 生成 |
| Apache PDFBox | 免费,简单易用 | 简单 PDF 生成 |
| Thymeleaf + Flying Saucer | 基于 HTML 模板生成 PDF | 需要 HTML 模板的场景 |
| OpenPDF | iText 的免费分支,轻量级 | 简单 PDF 生成 |
| JFreeReport + JFreeChart | 支持生成带图表的 PDF | 需要图表的 PDF 生成 |
2、开发过程
环境:SpringBoot 2.7.6、JDK1.8、itext 5.x
引入相关依赖
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.9</version>
<scope>compile</scope>
</dependency>
<!--中文字体问题解决-->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
准备好pdf,并且设置好表单字段。
直接打卡Adobe Acrobat,创建一个空白pdf,然后在工具里面,点击准备表单。实际上,在创建pdf时,可以选择word文件,因为直接编辑pdf格式很难调整,而格式调整好的word转成pdf就好办多了,在相应对方添加表单域就可以了。

按钮一栏可以自行摸索,添加相应的域,这里就不多解释了。注意pic字段,我用的依然是文本域,而不是图片域,因为代码不一样,这个后面再说。

注意,复选框要设置选中显示的值,也就是图中的导出值,样式也可以设置。
编写代码
PdfUtils的主要方法如下:
/**
* 通过pdf模板输出到流
*
* @param templateFile 模板
* @param dataMap input数据
* @param picData image图片
* @param checkboxMap checkbox勾选框
* @param params 设置属性,字体大小、颜色 textfont/textcolor/textsize等
* @param outputStream 输出流
*/
public static void output(String templateFile, Map<String, Object> dataMap, Map<String, byte[]>picData,
Map<String, byte[]> picData2,Map<String, String> checkboxMap,
Map<String,Object> params,OutputStream outputStream) {
PdfStamper ps = null;
PdfReader reader = null;
try {
reader = new PdfReader(templateFile);
ps = new PdfStamper(reader, outputStream);
AcroFields form = ps.getAcroFields();
BaseFont bf = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
form.addSubstitutionFont(bf);
if (ObjectUtils.isNotEmpty(dataMap)) {
for (String key : dataMap.keySet()) {
form.setField(key, String.valueOf(dataMap.getOrDefault(key, "")));
}
}
//不能编辑
ps.setFormFlattening(true);
if (ObjectUtils.isNotEmpty(checkboxMap)) {
for (String key : checkboxMap.keySet()) {
form.setField(key, checkboxMap.get(key), true);
}
}
PdfStamper stamper = ps;
if (ObjectUtils.isNotEmpty(picData)) {
picData.forEach((filedName, imgSrc) -> {
List<AcroFields.FieldPosition> fieldPositions = form.getFieldPositions(filedName);
for (AcroFields.FieldPosition fieldPosition : fieldPositions) {
int pageNo = fieldPosition.page;
Rectangle signRect = fieldPosition.position;
float x = signRect.getLeft();
float y = signRect.getBottom();
try {
Image image = Image.getInstance(imgSrc);
PdfContentByte under = stamper.getOverContent(pageNo);
image.scaleToFit(signRect.getWidth(), signRect.getHeight());
image.setAbsolutePosition(x, y);
under.addImage(image);
} catch (BadElementException e) {
log.error("pdf模板,文档元素错误 ", e);
} catch (IOException e) {
log.error("pdf模板,IO错误 ", e);
} catch (DocumentException e) {
log.error("pdf模板,文档操作错误 ", e);
}
}
});
}
if (!ObjectUtils.isEmpty(picData2)) {
picData2.forEach((filedName, imgSrc) -> {
List<AcroFields.FieldPosition> fieldPositions = form.getFieldPositions(filedName);
for (AcroFields.FieldPosition fieldPosition : fieldPositions) {
int pageNo = fieldPosition.page;
Rectangle signRect = fieldPosition.position;
float x = signRect.getLeft();
float y = signRect.getBottom();
try {
URL url = new URL(imgSrc);
Image image = Image.getInstance(url);
PdfContentByte under = stamper.getOverContent(pageNo);
image.scaleToFit(signRect.getWidth(), signRect.getHeight());
image.setAbsolutePosition(x, y);
under.addImage(image);
} catch (BadElementException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
}
}
});
}
} catch (Exception e) {
log.error("pdf模板填充数据异常 ", e);
} finally {
if (Objects.nonNull(ps)) {
try {
ps.close();
} catch (Exception e) {
log.error("pdf模板关闭异常 ", e);
}
}
if (Objects.nonNull(reader)) {
try {
reader.close();
} catch (Exception e) {
log.error("pdf模板关闭异常 ", e);
}
}
}
}
参数说明:
| 参数名称 | 参数类型 | 值 | 说明 |
|---|---|---|---|
| templateFile | String | /xx/xx/template.pdf | pdf模板,全路径 |
| dataMap | Map<String, Object> | { “name”: “John”, “age”: 30, “address”: “北京”} | 文本字段数据的map |
| picData | Map<String, byte[]> | {“pic1”:”xxx”} | 图片的数据map,其中byte[]是图片文件转成的字节数组 |
| picData2 | Map<String, String> | {“pic2”:”xxx”} | 图片的数据map,其中值是图片的url地址 |
| checkboxMap | Map<String, String> | {“checkbox1”:”1”} | 复选框的数据map。1代表选中,这个是在编辑pdf模板时设置的。 |
| params | Map<String,Object> | {“textsize”:”12f”} | 设置属性,字体大小、颜色 textfont/textcolor/textsize等 |
| outputStream | OutputStream | 生成的pdf后,输出流 | |
| … |
接口代码:
@GetMapping("/previewPdf")
public void previewPdf(HttpServletRequest request, HttpServletResponse response) throws IOException {
String templateFile = "D:\\workspace\\IdeaProjects\\pdfDemo\\src\\main\\resources\\static\\template.pdf";
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("name", "张小凡");
dataMap.put("age", "101");
dataMap.put("address", "上海市宛平南路600号");
dataMap.put("year", LocalDate.now().getYear());
dataMap.put("month", LocalDate.now().getMonthValue());
dataMap.put("day", LocalDate.now().getDayOfMonth());
response.setCharacterEncoding("utf-8");
response.setContentType("application/pdf");
Map<String, byte[]> picMap = new HashMap<>();
byte[] imageToBytes = ImageUtil.imageToBytes("D:\\workspace\\IdeaProjects\\pdfDemo\\src\\main\\resources\\static\\33981008.jpg");
picMap.put("pic", imageToBytes);
Map<String, String> picMap2 = new HashMap<>();
picMap2.put("pic2", "https://tse3-mm.cn.bing.net/th/id/OIP-C.jdP04yEoxG10mcywseQj7gAAAA?rs=1&pid=ImgDetMain");
Map<String, String> checkboxMap = new HashMap<>();
checkboxMap.put("smoke", "1");
checkboxMap.put("drink", "1");
checkboxMap.put("hair", "0");
String fileName = new String("测试预览pdf文件".getBytes(), "ISO-8859-1");//避免中文乱码
response.setHeader("Content-Disposition", "inline;filename=".concat(String.valueOf(fileName) + ".pdf"));
PdfUtil.output(templateFile, dataMap, picMap, picMap2,checkboxMap, response.getOutputStream());
}
下载的接口只要修改一行代码:
response.setHeader("Content-Disposition", "attachment;filename=".concat(fileName + ".pdf"));
结果展示:

ok,完美
3、代码解释以及一些问题
获取表单的所有字段:
AcroFields form = ps.getAcroFields();
创建BaseFont对象,用于加载字体
BaseFont bf = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
第一个参数name,是字体名称。”STSongStd-Light” 来自iTextAsian.jar,iTextAsian.jar 里面预制了各种字体,用于中日韩等文字的显示问题。”UniGB-UCS2-H”是对应的字符编码。BaseFont.NOT_EMBEDDED 字体不会嵌入到PDF文件中(没有保存),可能会导致不同电脑显示不正确。BaseFont.EMBEDDED 使用的字体会嵌入(保存)到PDF文件中,这样文件会更大一些,这样PDF文件显示就是跨平台的

添加一个替代字体。这种方法用于指定一个或多个备用字体,当PDF中的字段需要显示的字符在原始字体中找不到时,iText将尝试使用这些备用字体显示字符.
form.addSubstitutionFont(bf);
设置字段值以及字体样式、大小等:
form.setField(key, String.valueOf(dataMap.getOrDefault(key, "")));
form.setFieldProperty(key, "textfont", bf,null);
form.setFieldProperty(key, "textsize", 15.95f,null);
//重新生成字段以应用属性更改
form.regenerateField(key);
注意:form.regenerateField(key);这行代码是必须的,不然设置不会生效。
设置文本域不能编辑:
//不能编辑
ps.setFormFlattening(true);
图片展示的两种实现:
1、图片流的实现
Image image = Image.getInstance(imgSrc);
PdfContentByte under = stamper.getOverContent(pageNo);
image.scaleToFit(signRect.getWidth(), signRect.getHeight());
image.setAbsolutePosition(x, y);
under.addImage(image);
2、图片url的实现:
URL url = new URL(imgSrc);
Image image = Image.getInstance(url);
PdfContentByte under = stamper.getOverContent(pageNo);
image.scaleToFit(signRect.getWidth(), signRect.getHeight());
image.setAbsolutePosition(x, y);
under.addImage(image);
字体的问题:
如果想使用iTextAsian不包含的字体怎么办?
1、使用Windows系统下的字体
path是字体文件的全路径。
String path = "C:\\Windows\\Fonts\\simhei.ttf";
BaseFont font = BaseFont.createFont(path, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
Windows系统下的字体有的是.ttf类型,有的是.ttc类型。ttc类型的是组合字体,里面包含了几个字体的集合。若想使用ttc类型的文件,可以打开ttc文件,点击右上角下一个,看看你需要的是第几个字体,在声明字体的时候标注索引就好了。
String path = "C:\\Windows\\Fonts\\simhei.ttf 0";
BaseFont font = BaseFont.createFont(path, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
2、使用外部的字体
自己从网上下载字体,然后放在项目的classpath路径下:比如我的黑体文件路径:src/main/resources/fonts/simhei.ttf
BaseFont bf = BaseFont.createFont("/fonts/simhei.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
或者使用字体流的方式:
// 使用类加载器获取字体文件的输入流
InputStream fontStream = PdfUtil.class.getResourceAsStream("/fonts/simsun.ttf");
if (Objects.isNull(fontStream)) {
throw new RuntimeException("找不到字体文件");
}
byte[] fontBytes = IoUtil.readBytes(fontStream);
// 使用输入流创建BaseFont对象
BaseFont bf = BaseFont.createFont("simsun.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, true, fontBytes, null);
注意:这个时候name参数是字体的文件名称带后缀。
这种方式是比较推荐的,因为项目部署到不同平台,服务器自带的字体可能不支持,所以最好是放在项目中,然后嵌入pdf中。
3、使用iTextAsian的字体,或者itext自带字体(不支持中文)
BaseFont bf = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
BaseFont bf = BaseFont.createFont(BaseFont.HELVETICA, "Cp1252", BaseFont.NOT_EMBEDDED);
浏览器预览时,标签页的问题:
String fileName = new String("测试预览pdf文件".getBytes(), "ISO-8859-1");//避免中文乱码
response.setHeader("Content-Disposition", "inline;filename=".concat(String.valueOf(fileName) + ".pdf"));
虽然代码中设置了fileName,但是标签页却无法展示。
原因是标签页默认展示的是文档属性里面的标题。

如果不想修改文档属性的标题,可以在代码中修改重新设置标题。
// 获取文档信息
Map<String, String> info = reader.getInfo();
if (info == null) {
info = new HashMap<>();
}
// 修改标题,默认显示在浏览器的标签页上,如果不设置,标签页会显示原先文档属性的标题(adobe,打开文件,属性,设置标题)
info.put("Title", "这是一个pdf");
// 设置文档信息
ps.setMoreInfo(info);
效果:

4、使用itext5实现html转pdf
由于pdf模板无法做到word那样根据内容大小自动换行,只能提前预估内容,设置文本域高度和宽度,特别是下划线上填充内容时,一行显示不下,不能自动换行,所以一些情况下,pdf模板填充内容无法满足需求。这时候考虑使用word模板然后转pdf,或者使用html转pdf。
itext5可以实现html转pdf。引入新的依赖:
<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.13.4</version>
</dependency>
html可以是一个真实的文件,也可以是html字符串,还可以用模板引擎。最后都是转成流参数。简单实例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<style>
body {
font-family: "SimSun", "Microsoft YaHei", sans-serif;
}
</style>
</head>
<body>
<h1 style="text-align: center">标题</h1>
<p>这是一段测试文本!</p>
<p style="text-align: left;color: red">这是第二段测试文本!</p>
<div>
<img src="http://www.baidu.com/img/bdlogo.gif" alt="百度" title="百度" width="100" height="100"/>
</div>
</body>
</html>
@GetMapping("/previewPdf7")
public void previewPdf7(HttpServletRequest request, HttpServletResponse response) throws IOException, DocumentException {
// 创建 PDF 文档
Document document = new Document();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = PdfWriter.getInstance(document, baos);
document.open();
// 读取 HTML 文件
// FileInputStream htmlFile = new FileInputStream("input.html");
String html = "<html><body><h1>Hello, World!</h1></body></html>";
InputStream htmlStream = new ByteArrayInputStream(html.getBytes(StandardCharsets.UTF_8));
// 从类路径加载 HTML 文件
InputStream htmlFile = MyPdfController.class.getResourceAsStream("/static/index2.html");
document.addTitle("html转pdf测试");
FontProvider asianFontProvider = new FontProvider() {
@Override
public boolean isRegistered(String s) {
return false;
}
@Override
public Font getFont(String s, String s1, boolean b, float v, int i, BaseColor baseColor) {
BaseFont bfChinese = null;
try {
bfChinese = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
} catch (DocumentException | IOException e) {
throw new RuntimeException(e);
}
return new Font(bfChinese, v, i, baseColor);
}
};
// 使用 XMLWorkerHelper 将 HTML 转换为 PDF
// XMLWorkerHelper.getInstance().parseXHtml(writer, document, htmlFile, StandardCharsets.UTF_8, new AsianFontProvider());
XMLWorkerHelper.getInstance().parseXHtml(writer, document, htmlFile, StandardCharsets.UTF_8, asianFontProvider);
// 关闭文档
document.close();
// 获取 PDF 字节流
byte[] pdfBytes = baos.toByteArray();
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.setContentType(MediaType.APPLICATION_PDF_VALUE);
String fileName = new String("test".getBytes(), StandardCharsets.ISO_8859_1);
response.setHeader(Header.CONTENT_DISPOSITION.getValue(), "inline;filename=".concat(fileName + ".pdf"));
// 将 PDF 字节流写入响应输出流
OutputStream out = response.getOutputStream();
out.write(pdfBytes);
out.flush();
out.close();
}
注意中文的问题,需要FontProvider加载中文字体才能显示中文,否则会乱码。也可以单独写一个中文的class,然后,当成参数,传给parseXHtml方法。比如:
/**
* 用于中文显示的Provider
*/
class AsianFontProvider extends XMLWorkerFontProvider {
@Override
public Font getFont(final String fontname, String encoding, float size, final int style) {
try {
BaseFont bfChinese = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
return new Font(bfChinese, size, style);
} catch (Exception e) {
}
return super.getFont(fontname, encoding, size, style);
}
}
字体可以是iTextAsian中的,也可以自定义加载字体,这个和前面的一样。效果如下:

到这里还没结束,简单的html和简单样式可以,但是稍微复杂的样式,itext5就不支持了,所以样式多的html用itext5效果很不好。比如这样一个html页面转成pdf后,效果很差。

pdf:

最重要的是表格显示不全,也许调整html样式可以解决,但是太麻烦,itext5对css样式支持有限是主要问题。text7支持更多的css样式,但是有开源协议的限制。
继续尝试其他的转pdf工具,比如:
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-itext5</artifactId>
<version>9.1.22</version>
</dependency>
但是效果一样不好,而且需要在html中指定字体,不然中文显示不出来。
<style>
body {
font-family: "SimSun", "Microsoft YaHei", sans-serif;
}
</style>

最后使用wkhtmltox工具,经过测试,这个工具转pdf效果好,所以采用这个方案,缺点是需要在服务器上安装工具、还有字体。
用工具转pdf后,可以直接保存到服务器上,也可以返回文件流。
@GetMapping("/previewPdf8")
public void previewPdf8(HttpServletResponse response) {
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.setContentType(MediaType.APPLICATION_PDF_VALUE);
// HTML 内容
String htmlContent = "<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\"/>\n" +
"</head>\n" +
"<body> <div data-v-137a61c0=\"\" style=\"text-align: center; font-size: 20px; font-weight: bold; margin-bottom: 30px;\"> xxx患者病历单 </div><table data-v-137a61c0=\"\" cellspacing=\"0\" style=\"width: 100%; border-collapse: collapse;\"><tr data-v-137a61c0=\"\" style=\"border: 1px solid rgb(102, 102, 102); min-height: 40px;\"><td data-v-137a61c0=\"\" colspan=\"6\" style=\"background: lightblue; line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 200px;\">患者基本信息</td></tr><tr data-v-137a61c0=\"\" style=\"border: 1px solid rgb(102, 102, 102); min-height: 40px;\"><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 100px;\">姓名</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 200px;\">test012</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 100px;\">性别</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 200px;\">男</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 100px;\">证件号</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 200px;\"></td></tr><tr data-v-137a61c0=\"\" style=\"border: 1px solid rgb(102, 102, 102); min-height: 40px;\"><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 100px;\">年龄</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 200px;\"></td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 100px;\">婚姻</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 200px;\"> </td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 100px;\">身高</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 200px;\"> </td></tr><tr data-v-137a61c0=\"\" style=\"border: 1px solid rgb(102, 102, 102); min-height: 40px;\"><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 100px;\">体重</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 200px;\"> </td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 100px;\">民族</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 200px;\"></td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 100px;\">就诊ID</td><td data-v-137a61c0=\"\" style=\"line-height: 30px; padding: 5px 10px; border-spacing: 0px; border-right: 1px solid rgb(102, 102, 102); font-size: 14px; width: 200px;\"></td></tr></table><div data-v-137a61c0=\"\" style=\"font-size: 12px; color: rgb(102, 102, 102); margin: 20px 0px;\"> 注:远程诊疗是异地医疗咨询活动,门诊医师与申请门诊医师之间的关系属于医学知识的咨询关系,对病人的诊断与治疗的决定权属于收治病人的医疗机构(引自原卫生部卫办发[1999]第2号)。 </div>" +
"</body>\n" +
"</html>";
// 调用 wkhtmltopdf 将 HTML 字符串转换为 PDF 字节流
try {
byte[] pdfBytes = htmlToPdf(htmlContent);
System.out.println("PDF 生成成功!大小: " + pdfBytes.length + " 字节");
//避免中文乱码
String fileName = new String("test".getBytes(), StandardCharsets.ISO_8859_1);
response.setHeader("Content-Disposition", "inline;filename=".concat(fileName) + ".pdf");
OutputStream outputStream = response.getOutputStream();
outputStream.write(pdfBytes);
outputStream.flush();
outputStream.close();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
public byte[] htmlToPdf(String htmlContent) throws IOException, InterruptedException {
// 创建 ProcessBuilder
ProcessBuilder processBuilder = new ProcessBuilder("wkhtmltopdf", "-", "-");
processBuilder.redirectErrorStream(true);
// 启动进程
Process process = processBuilder.start();
// 将 HTML 内容写入进程的标准输入
try (OutputStream stdin = process.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin))) {
writer.write(htmlContent);
}
// 捕获进程的标准输出(PDF 字节流)
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (InputStream stdout = process.getInputStream()) {
byte[] buffer = new byte[1024];
int length;
while ((length = stdout.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
}
// 等待进程完成
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("wkhtmltopdf 进程执行失败,退出码: " + exitCode);
}
// 返回 PDF 字节流
return outputStream.toByteArray();
}
效果如下:

工具的效果还是比较好的。