背景
Springboot 中通过组件依赖实现构建PDF
依赖组件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.22</version>
</dependency>
<dependency>
<groupId>com.lowagie</groupId>
<artifactId>itext</artifactId>
<version>2.1.7</version>
</dependency>
关联代码
1. PDF 模版
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>粒径分析汇总报告</title>
<style>
@page { size: A4 portrait; margin: 1cm; }
body {
font-family: "SimSun", serif;
font-size: 11px;
color: #333;
margin: 0;
padding: 0;
background: linear-gradient(to bottom, #f8fafc 0%, #ffffff 3%);
background-repeat: no-repeat;
background-attachment: fixed;
}
table {
border: none;
border-collapse: collapse;
border-spacing: 0;
}
h1 {
font-size: 18px;
text-align: center;
margin: 4px 0 8px;
padding: 8px 0;
border-bottom: 2px solid #2c5aa0;
color: #1a365d;
background: linear-gradient(to right, #ebf4ff, #ffffff, #ebf4ff);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
h2 {
font-size: 14px;
margin: 15px 0 4px;
padding: 4px 8px;
border-bottom: 1px solid #c3dafe;
color: #2c5282;
background-color: #ebf4ff;
border-radius: 3px;
}
table { width: 100%; border-collapse: collapse; }
th, td {
border: 1px solid #cbd5e0;
padding: 3px 2px;
text-align: center;
}
th {
background: linear-gradient(to bottom, #ebf4ff, #c3dafe);
font-weight: bold;
color: #2c5282;
}
.info-table {
margin: 6px 0;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.info-table th {
width: 15%;
background: linear-gradient(to bottom, #e6fffa, #b2f5ea);
color: #234e52;
}
.info-table td {
width: 35%;
background-color: #f0fff4;
}
.info-table tr:hover td {
background-color: #e6fffa;
}
.dist-table {
margin: 6px 0;
page-break-inside: avoid;
}
.dist-table td {
vertical-align: top;
border: none;
padding: 0 3px;
}
.dist-left {
width: 50%;
}
.dist-right {
width:40 %;
height: auto;
text-align: center;
}
.data-table {
font-size: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.data-table th {
background: linear-gradient(to bottom, #feebc8, #fbd38d);
color: #744210;
}
.data-table td {
border: 1px solid #fbd38d;
background-color: #fffbeb;
}
.data-table tr:nth-child(even) td {
background-color: #fefcbf;
}
.chart-img {
width: 260px;
height: auto;
border: 1px solid #cbd5e0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background-color: white;
}
.caption {
margin-top: 4px;
font-weight: bold;
font-size: 10px;
color: #4a5568;
}
.result-table {
margin: 6px 0;
page-break-inside: avoid;
}
.result-table td {
vertical-align: top;
padding: 3px 2px;
text-align: center;
}
.result-right-img {
width: 260px;
text-align: center;
}
.result-right-img img,
.result-img {
width: 710px;
height: 400px;
display: block;
}
.vertical-table {
width: 100%;
font-size: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.vertical-table th {
background: linear-gradient(to bottom, #e9d8fd, #d6bcfa);
color: #44337a;
}
.vertical-table td {
font-weight: bold;
background-color: #faf5ff;
}
.vertical-table tr:nth-child(even) td {
background-color: #fefcbf !important;
}
.result-img {
height: auto;
width: 260px;
border: 1px solid #d6bcfa;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background-color: white;
}
.highlight {
background-color: #fefcbf !important;
color: #744210;
font-weight: bold;
}
</style>
</head>
<body>
<h1>XXXXXX</h1>
<table class="info-table">
<tr>
<th>样品种类</th>
<td th:text="${instrumentType} ?: 'AFM'">AFM</td>
<th>项目号</th>
<td th:text="${projectNo} ?: '-'">-</td>
</tr>
<tr>
<th>部门ID</th>
<td th:text="${departmentId} ?: '1'">1</td>
<th>日期</th>
<td th:text="${date} ?: '2025-08-01'">2025-08-01</td>
</tr>
<tr>
<th>送样人</th>
<td th:text="${userName} ?: 'xxxxx'">xxxxx</td>
<th>电话</th>
<td th:text="${phoneNumber} ?: '-'">-</td>
</tr>
<tr>
<th>样品测试条件</th>
<td colspan="3" th:text="${detectionCondition} ?: '-'">-</td>
</tr>
<tr>
<th>样品研究目的</th>
<td colspan="3" th:text="${detectionPurpose} ?: '-'">-</td>
</tr>
</table>
<h2>XXX<span style="font-size: 28px;">概况</span></h2>
<table class="vertical-table">
<tr>
<th>最大粒径(μm)</th>
<td class="highlight" th:text="${max} ?: '0.8673'">0.8673</td>
<th>样本数量</th>
<td class="highlight" th:text="${sampleCount} ?: '837'">837</td>
</tr>
<tr>
<th>最小粒径(μm)</th>
<td class="highlight" th:text="${min} ?: '0.0003'">0.0003</td>
<th>样本组距(μm)</th>
<td class="highlight" th:text="${binWidth} ?: '0.05'">0.05</td>
</tr>
<tr>
<th>平均粒径(μm)</th>
<td class="highlight" th:text="${mean} ?: '0.083'">0.083</td>
<th>样本组数</th>
<td class="highlight" th:text="${binCount} ?: '18'">18</td>
</tr>
</table>
<h2>XXXX<span style="font-size: 28px;">直方分布</span></h2>
<table class="dist-table">
<tr>
<td class="dist-left">
<table class="data-table">
<thead>
<tr>
<th>区间(μm)</th>
<th>平均值</th>
<th>数量</th>
<th>频率(%)</th>
</tr>
</thead>
<tbody>
<tr th:each="r : ${bins}">
<td th:text="${r.range}">0.00-0.05</td>
<td th:text="${r.midpoint}">0.025</td>
<td th:text="${r.count}">358</td>
<td th:text="${#numbers.formatDecimal(r.frequency,1,1)}">42.8</td>
</tr>
</tbody>
</table>
</td>
<td class="dist-right">
<img th:src="${summaryImageBase64}" class="chart-img" alt="统计条形图"/>
<div class="caption">统计条形图</div>
</td>
</tr>
</table>
<h2>XXXX<span style="font-size: 28px;">样本图</span></h2>
<table class="result-table">
<tr>
<td class="result-right-img">
<img th:src="${sampleImageBase64}" class="result-img" alt="分析结果示意图"/>
<div class="caption">分析结果示意图</div>
</td>
</tr>
</table>
</body>
</html>
2. PDF 构建接口
@GetMapping(value = "/{summaryId}/pdf", produces = MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<byte[]> preview(@PathVariable("summaryId") Long summaryId) throws Exception {
PdfVO pdfVO = tSummaryServiceImpl.generatePDFBySummaryId(summaryId);
byte[] pdf = createPdf(pdfVO);
String fileName = URLEncoder.encode(pdfVO.getProjectNo() + "_XXXXX.pdf", "UTF-8")
.replaceAll("\+", "%20");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; filename="" + fileName + ""; filename*=UTF-8''" + fileName)
.contentType(MediaType.APPLICATION_PDF)
.body(pdf);
}
private byte[] createPdf(PdfVO pdfVO) throws Exception {
Context ctx = new Context();
ctx.setVariable("instrumentType", pdfVO.getInstrumentType());
ctx.setVariable("projectNo", pdfVO.getProjectNo());
ctx.setVariable("departmentId", pdfVO.getDepartmentId());
ctx.setVariable("date", pdfVO.getDate());
ctx.setVariable("userName", pdfVO.getUserName());
ctx.setVariable("phoneNumber", pdfVO.getPhoneNumber());
ctx.setVariable("detectionCondition", pdfVO.getDetectionCondition());
ctx.setVariable("detectionPurpose", pdfVO.getDetectionPurpose());
ctx.setVariable("sampleImageBase64", "data:image/jpeg;base64," + pdfVO.getSampleImageBase64());
ctx.setVariable("summaryImageBase64", "data:image/jpeg;base64," + pdfVO.getSummaryImageBase64());
ctx.setVariable("max", pdfVO.getMax());
ctx.setVariable("min", pdfVO.getMin());
ctx.setVariable("sampleCount", pdfVO.getSampleCount());
ctx.setVariable("binWidth", pdfVO.getBinWidth());
ctx.setVariable("binCount", pdfVO.getBinCount());
ctx.setVariable("bins", pdfVO.getBins());
String html = templateEngine.process("report-01", ctx);
return PdfUtil.genPdf(html);
}
3. PDF 构建类
package com.pcitc.common.utils
import com.lowagie.text.pdf.BaseFont
import org.xhtmlrenderer.pdf.ITextRenderer
import java.io.ByteArrayOutputStream
public class PdfUtil {
public static byte[] genPdf(String html) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream()
ITextRenderer renderer = new ITextRenderer()
renderer.getSharedContext().setReplacedElementFactory(
new Base64ImageReplacedElementFactory(renderer.getSharedContext().getReplacedElementFactory())
)
/* 中文字体 */
String fontPath = PdfUtil.class.getResource("/fonts/SimSun.ttf").toString()
renderer.getFontResolver().addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED)
renderer.setDocumentFromString(html)
renderer.layout()
renderer.createPDF(out)
return out.toByteArray()
}
}
4. 处理PDF 中存在图片,但是不显示的问题。
package com.pcitc.common.utils;
import com.lowagie.text.Image;
import org.springframework.util.Base64Utils;
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 javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
public class Base64ImageReplacedElementFactory implements ReplacedElementFactory {
private final ReplacedElementFactory superFactory;
public Base64ImageReplacedElementFactory(ReplacedElementFactory superFactory) {
this.superFactory = superFactory;
}
@Override
public ReplacedElement createReplacedElement(LayoutContext layoutContext, BlockBox blockBox,
UserAgentCallback userAgentCallback, int cssWidth, int cssHeight) {
org.w3c.dom.Element element = blockBox.getElement();
if (element == null) {
return null;
}
String nodeName = element.getNodeName();
if ("img".equals(nodeName)) {
String src = element.getAttribute("src");
if (src.startsWith("data:image/")) {
String base64Data = src.substring(src.indexOf("base64,") + 7);
byte[] imageBytes = Base64Utils.decodeFromString(base64Data.replace("\s", ""));
try {
BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(imageBytes));
if (bufferedImage == null) {
throw new IOException("ImageIO could not decode image from byte array");
}
Image image = Image.getInstance(bufferedImage, null);
FSImage fsImage = new ITextFSImage(image);
if (cssWidth != -1 || cssHeight != -1) {
fsImage.scale(cssWidth, cssHeight);
}
return new ITextImageElement(fsImage);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
return superFactory.createReplacedElement(layoutContext, blockBox, userAgentCallback, cssWidth, cssHeight);
}
@Override
public void reset() {
superFactory.reset();
}
@Override
public void remove(org.w3c.dom.Element e) {
superFactory.remove(e);
}
@Override
public void setFormSubmissionListener(FormSubmissionListener listener) {
superFactory.setFormSubmissionListener(listener);
}
}