引入依賴
<!--Freemarker wls-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.11</version>
</dependency>
<!-- 支援中文 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<!-- 支援css樣式渲染 -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-itext5</artifactId>
<version>9.1.18</version>
</dependency>
<dependency>
<groupId>gui.ava</groupId>
<artifactId>html2image</artifactId>
<version>2.0.1</version>
</dependency>
程式碼示例
後端程式碼
/**
* @author alin
* @date 2024-06-11
*/
@Slf4j
public class TestCreatePdf {
public static void main(String[] args) throws Exception {
generatePdfUrl();
}
/**
* 生成pdf
*
* @return
*/
public static String generatePdfUrl() throws Exception {
// 構造引數
Model model = assembleData();
return createPdfAndUpload(beanToMap(model), UUID.randomUUID() + ".pdf", "testCreatePdf.html", "testCreatePdf.css");
}
/**
* 建立pdf並上傳/輸出
*
* @param data
* @param fileName 檔名
* @param templateFileName 模板檔名, html模板檔案
* @param cssPath css檔案路徑
* @return
* @throws Exception
*/
private static String createPdfAndUpload(Map<String, Object> data, String fileName, String templateFileName, String cssPath) throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
// 根據模板生成html字串
String pdf = createHtmlStr(data, templateFileName);
// 透過html字串生成pdf檔案
generatePdf(pdf, outputStream, cssPath);
} catch (Exception e) {
return null;
}
// 輸出/上傳至指定位置
FileOutputStream out = new FileOutputStream("d:/testPdf/" + fileName);
out.write(outputStream.toByteArray());
return "d:/testPdf/" + fileName;
}
/**
* bean轉map
*
* @param bean
* @return
*/
public static Map<String, Object> beanToMap(Object bean) {
Class<?> clazz = bean.getClass();
return Arrays.stream(clazz.getDeclaredFields())
.collect(Collectors.toMap(
Field::getName,
field -> {
try {
field.setAccessible(true);
return field.get(bean);
} catch (IllegalAccessException e) {
// 處理異常
return null;
}
}
));
}
/**
* 模板生成html字串
*
* @param data 資料
* @param templateFileName 模板檔名
* @throws Exception 捕獲異常
*/
public static String createHtmlStr(Map<String, Object> data, String templateFileName) throws Exception {
// 建立一個FreeMarker例項, 負責管理FreeMarker模板的Configuration例項
Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
// 指定FreeMarker模板檔案的位置
cfg.setClassForTemplateLoading(TestCreatePdf.class, "/template");
// 獲取模板檔案
Template template = cfg.getTemplate(templateFileName, "UTF-8");
StringWriter stringWriter = new StringWriter();
BufferedWriter writer = new BufferedWriter(stringWriter);
template.process(data, writer);
String htmlStr = stringWriter.toString();
writer.flush();
writer.close();
return htmlStr;
}
/**
* 透過html字串生成pdf檔案
*
* @param htmlStr
* @param out
* @param cssPath
* @throws IOException
* @throws DocumentException
*/
public static void generatePdf(String htmlStr, OutputStream out, String cssPath) throws IOException, DocumentException {
Document document = new Document(PageSize.A3);
PdfWriter writer = PdfWriter.getInstance(document, out);
document.open();
// html內容解析
HtmlPipelineContext htmlContext = new HtmlPipelineContext(
new CssAppliersImpl(new XMLWorkerFontProvider() {
@Override
public Font getFont(String fontName, String encoding,
float size, final int style) {
Font font = null;
if (fontName == null) {
//字型
BaseFont bf;
try {
bf = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
font = new Font(bf, size, style);
} catch (Exception e) {
log.error("getFont", e);
}
}
return font;
}
})) {
@Override
public HtmlPipelineContext clone()
throws CloneNotSupportedException {
HtmlPipelineContext context = super.clone();
try {
ImageProvider imageProvider = this.getImageProvider();
context.setImageProvider(imageProvider);
} catch (Exception e) {
log.error("clone", e);
}
return context;
}
};
// 圖片解析
htmlContext.setImageProvider(new AbstractImageProvider() {
@Override
public String getImageRootPath() {
return StringUtils.EMPTY;
}
@Override
public Image retrieve(String src) {
if (StringUtils.isEmpty(src)) {
return null;
}
try {
int pos = src.indexOf("base64,");
try {
if (src.startsWith("data") && pos > 0) {
byte[] img = Base64.decode(src.substring(pos + 7));
return Image.getInstance(img);
} else if (src.startsWith("http")) {
return Image.getInstance(src);
}
} catch (Exception ex) {
log.error("retrieve", ex);
return null;
}
return null;
} catch (Throwable e) {
log.error("retrieve", e);
}
return super.retrieve(src);
}
});
htmlContext.setAcceptUnknown(true).autoBookmark(true).setTagFactory(Tags.getHtmlTagProcessorFactory());
// css解析
CSSResolver cssResolver = new StyleAttrCSSResolver();
InputStream cssInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(cssPath);
CssFile cssfile = XMLWorkerHelper.getCSS(cssInputStream);
cssResolver.addCss(cssfile);
HtmlPipeline htmlPipeline = new HtmlPipeline(htmlContext, new PdfWriterPipeline(document, writer));
Pipeline<?> pipeline = new CssResolverPipeline(cssResolver, htmlPipeline);
XMLWorker worker = new XMLWorker(pipeline, true);
XMLParser parser = new XMLParser(true, worker, StandardCharsets.UTF_8);
try (InputStream inputStream = new ByteArrayInputStream(
htmlStr.getBytes())) {
parser.parse(inputStream, StandardCharsets.UTF_8);
}
document.close();
}
/**
* 組裝資料
*
* @return
* @throws Exception
*/
private static Model assembleData() throws Exception {
TestCreatePdf.Model model = new TestCreatePdf.Model();
model.setCompanyName("公司名稱");
model.setField1("欄位一");
model.setField2("欄位二");
model.setField3("欄位三");
model.setField4("欄位四");
model.setField5("欄位五");
model.setField6("欄位六");
model.setField7("欄位七");
model.setRemark("備註~~~~~~~~~");
model.setSignUrl1("D:/testPdf/test.png");
model.setSignUrl2("D:/testPdf/test.png");
model.setSignUrl3("D:/testPdf/test.png");
model.setSignTime1("2024-04-28 17:08:52");
model.setSignTime2("2024-04-28 17:08:52");
model.setSignTime3("2024-04-28 17:08:52");
List<Object> modeDetailFieldList = Lists.newArrayList();
modeDetailFieldList.add("表頭一");
modeDetailFieldList.add("表頭二");
modeDetailFieldList.add("表頭三");
modeDetailFieldList.add("表頭四");
modeDetailFieldList.add("表頭五");
modeDetailFieldList.add("表頭六");
model.setModeDetailFieldList(modeDetailFieldList);
List<List<Object>> modeDetailValueList = Lists.newArrayList();
for (int i = 1; i < 6; i++) {
List<Object> valueList = Lists.newArrayList();
valueList.add("表頭一值--" + i);
valueList.add("表頭二值--" + i);
valueList.add("表頭三值--" + i);
valueList.add("表頭四值--" + i);
valueList.add("表頭五值--" + i);
valueList.add("表頭六值--" + i);
modeDetailValueList.add(valueList);
}
model.setModeDetailValueList(modeDetailValueList);
return model;
}
@Data
public static class Model {
/**
* companyName
*/
private String companyName;
/**
* 欄位1
*/
private String field1;
/**
* 欄位2
*/
private String field2;
/**
* 欄位3
*/
private String field3;
/**
* 欄位4
*/
private String field4;
/**
* 欄位5
*/
private String field5;
/**
* 欄位6
*/
private String field6;
/**
* 欄位7
*/
private String field7;
/**
* 備註
*/
private String remark;
/**
* 圖片地址(base64結構)
*/
private String imgBase64;
/**
* signUrl1
*/
private String signUrl1;
/**
* signUrl2
*/
private String signUrl2;
/**
* signUrl3
*/
private String signUrl3;
/**
* signTime1
*/
private String signTime1;
/**
* signTime2
*/
private String signTime2;
/**
* signTime3
*/
private String signTime3;
/**
* 表格欄位名稱
*/
private List<Object> modeDetailFieldList;
/**
* 表格欄位值
*/
private List<List<Object>> modeDetailValueList;
}
}
模板及樣式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>測試流程表格檔案</title>
<link type="text/css" rel="stylesheet" href="style/testCreatePdf.css"/>
<style/>
</head>
<body>
<div class="app-container">
<div class="app-container-top">
<div class="title">測試流程表格檔案</div>
</div>
<div class="app-container-body">
<div class="park-label">${companyName}</div>
<div class="nbsp-5"></div>
<table class="baseInfo-table">
<tr>
<td style=" background-color: #F0F0F0; text-align: left; width:250px;">欄位1</td>
<td style="width:420px;">${field1}</td>
<td style=" background-color: #F0F0F0;text-align: left; width:250px;">欄位2</td>
<td style="width:400px;">${field2}</td>
<td style=" background-color: #F0F0F0;text-align: left; width:250px;">欄位3</td>
<td style="width:420px;">${field4}</td>
</tr>
<tr>
<td style=" background-color: #F0F0F0;text-align: left;">欄位4</td>
<td>${field4}</td>
<td style=" background-color: #F0F0F0;text-align: left;">欄位5</td>
<td>${field5}</td>
<td style=" background-color: #F0F0F0;text-align: left;">欄位6</td>
<td>${field6}</td>
</tr>
<tr>
<td style=" background-color: #F0F0F0;text-align: left;">欄位7</td>
<td colspan="5">${field7}</td>
</tr>
</table>
<table class="mode-table">
<thead>
<tr>
<#list modeDetailFieldList as obj >
<td>${obj}</td>
</#list>
</tr>
</thead>
<tbody>
<#list modeDetailValueList as detailValue >
<tr>
<#list detailValue as obj >
<td>${obj}</td>
</#list>
</tr>
</#list>
</tbody>
</table>
<div class="nbsp-20"></div>
<div class="remark-div">
<div class="remark">${remark}</div>
</div>
<div class="nbsp-20"></div>
<div class="sign">
<div class="sign-div-seal">
<div class="sign-div-title">簽字1:</div>
<div class="nbsp-40"></div>
<div style="width:100%;height:300px">
<div class="nbsp-width" style="width:20%"></div>
<div class="nbsp-width" style="width:70%">
<img width="100%" height="100%" src="${signUrl1}" />
</div>
</div>
<div class="sign-time">時間:${signTime1}</div>
</div>
<div class="sign-div-seal">
<div class="sign-div-title">簽字2:</div>
<div class="nbsp-40"></div>
<div style="width:100%;height:300px">
<div class="nbsp-width" style="width:20%"></div>
<div class="nbsp-width" style="width:70%">
<img width="100%" height="100%" src="${signUrl2}" />
</div>
</div>
<div class="sign-time">時間:${signTime3}</div>
</div>
<div class="sign-div-seal">
<div class="sign-div-title">簽字3:</div>
<div class="nbsp-40"></div>
<div style="width:100%;height:300px">
<div class="nbsp-width" style="width:20%"></div>
<div class="nbsp-width" style="width:70%">
<img width="100%" height="100%" src="${signUrl3}" />
</div>
</div>
<div class="sign-time">時間:${signTime3}</div>
</div>
</div>
</div>
</div>
</body>
</html>
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.app-container {
padding: 20px;
}
.app-container-top {
width:100%;
height:100px;
}
.nbsp-5 {
width: 100%;
height: 5px;
}
.nbsp-10 {
width: 100%;
height: 10px;
}
.nbsp-20 {
width: 100%;
height: 20px;
}
.nbsp-40 {
width: 100%;
height: 40px;
}
.nbsp-width {
height:300px;
float:left;
}
.nbsp-div {
width: 35%;
height: 100px;
float: left;
}
.logo-div {
width: 200px;
float: left;
height: 100px;
}
.logo-div img {
width: 200px;
height:40px;
margin: 10px 0 20px 0;
}
.qrcode-div {
width: 100px;
display: flex;
height: 100px;
float: right;
}
.qrcode-div img {
width: 100px;
object-fit: cover;
}
.title {
font-size: 30px;
height: 100px;
float: left;
text-align: center;
}
.park-label {
font-size: 20px;
height: 50px;
float: left;
}
.app-container-body {
margin-top: 5px;
}
table tr td {
height:40px;
}
.baseInfo-table {
width: 100%;
margin: 0 auto;
border: 1px solid #9D9D9D;
border-collapse: collapse;
}
.baseInfo-table tr td {
border: 1px solid #9D9D9D;
text-align: center;
}
.mode-table {
width: 100%;
margin: 10px 0 auto;
border: 1px solid #9D9D9D;
border-collapse: collapse;
}
.mode-table tr td {
border: 1px solid #9D9D9D;
text-align: center;
}
.mode-table tr td:nth-child(1) {
width: 50px;
}
.mode-table tr td:nth-child(2) {
width: 150px;
}
.mode-table tr td:nth-child(4) {
width: 80px;
}
.mode-table tr td:nth-child(8) {
width: 100px;
}
.mode-table tr td:nth-child(9) {
width: 100px;
}
.test-table {
margin-top: 10px;
width: 100%;
margin: 50px 0 auto;
border: 1px solid #9D9D9D;
border-collapse: collapse;
}
.test-table tr td {
border: 1px solid #9D9D9D;
text-align: center;
}
.test-table tr td:nth-child(1) {
width: 30px;
height: 60px;
}
.test-table tr td:nth-child(2) {
width: 120px;
}
.sign {
width: calc(100% - 80px);
margin: 40px;
}
.sign-div {
width: 50%;
height: 450px;
float: left;
text-align: left;
}
.sign-div-seal {
width: 325px;
height: 450px;
float: left;
text-align: left;
}
.sign-img {
height: 300px;
}
.sign-div-title {
text-align: left;
}
.sign-time {
margin-top: 10px;
text-align: left
}
效果圖