一.根据html模板生成pdf文档
说明:使用html模板,这里采用的是freemarker,ognl表达式,然后使用itextpdf读取模板,并动态填充业务数据到html模板中,最后将html转成pdf输出。
注意:
1.html模板编写中,存在部分样式丢失,如绝对定位相对定位等等。
2.该方式暂时没有直接获取对应输出的pdf总页码功能,需要用到另外一个工具。
3.htm模板中如果存在动态表格,使用ognl表达式进行遍历ide会提示报错(实际没问题)
4.动态填充的数据字段不能为空,否则程序也会报错。因此在填充动态数据之前,所有的字段都需要空判断。
实现步骤
1.添加依赖
<!--pdf预览添加依赖-->
<!-- 页面渲染模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ognl/ognl -->
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.2.10</version>
</dependency>
<!-- pdf -->
<!-- https://mvnrepository.com/artifact/com.itextpdf/itext-asian -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>2.1.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.itextpdf/barcodes -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>barcodes</artifactId>
<version>7.1.7</version>
<exclusions>
<exclusion>
<artifactId>io</artifactId>
<groupId>com.itextpdf</groupId>
</exclusion>
<exclusion>
<artifactId>kernel</artifactId>
<groupId>com.itextpdf</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 获取pdf页码 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.24</version>
</dependency>
2.下载字体和制作模板
字体文件:
html模板文件:
<!DOCTYPE html>
<html lang="en">
<head>
<style>
/* common */
table {
border-spacing: 0;
border-color: transparent;
width: 100%;
border-width: 1px;
border: none;
border-collapse: collapse;
}
tr {
width: 100%;
}
th {
padding: 10px;
border: 1px solid #c8c8c8;
font-size: 14px;
}
td {
padding: 8px;
padding-left: 24px;
border: 1px solid #c8c8c8;
font-size: 13px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.bg-head-1 {
background-color: #2986f1;
color: #ffffff;
}
.bg-head-2 {
background-color: #e9e9e9;
}
.color-3b3b3b{
color: #3B3B3B;
}
.content {
width: 904px;
margin: 0 auto;
background-size: 100%;
position: relative;
min-height: 100vh;
}
/* header */
.head-box {
width: 904px;
height: 107px;
margin: 0 auto;
margin-top: 20px;
}
.head-top {
width: 904px;
height: 100px;
margin: 0 auto;
}
.head-left {
width: 779px;
height: 100px;
line-height: 100px;
text-align: center;
float: left;
}
.title {
font-size: 24px;
color: #2986f1;
font-family: "Microsoft YaHei UI";
font-weight: 800;
margin-left: 120px;
}
.head-right {
width: 120px;
height: 100px;
float: left;
}
.qrcode-box {
width: 120px;
height: 80px;
text-align: center;
}
.qrcode-img {
width: 80px;
height: 80px;
}
.qrcode-img_ {
width: 280px;
height: 132PX;
}
.qrcode-txt-box {
width: 120px;
height: 20px;
text-align: center;
}
.txt {
font-size: 13px;
color: #959595;
}
.head-bottom {
width: 904px;
height: 7px;
margin: 0 auto;
margin-top: 10px;
}
.separate-left {
width: 270px;
height: 7px;
background-color: #d0d0d0;
transform: skewX(-45deg);
float: left;
}
.separate-right {
width: 634px;
height: 7px;
background-color: #2986f1;
transform: skewX(-45deg);
float: left;
}
/* 基本信息 */
.base-box {
width: 904px;
height: auto;
margin: 0 auto;
margin-top: 20px;
}
/* 主要总成和系统的主要零部件种类范围 */
.main-box {
width: 904px;
height: auto;
margin: 0 auto;
margin-top: 20px;
}
.easy-box {
width: 904px;
height: auto;
margin: 0 auto;
margin-top: 20px;
}
.special-box {
width: 904px;
height: auto;
margin: 0 auto;
margin-top: 20px;
}
.power-box {
width: 904px;
height: auto;
margin: 0 auto;
margin-top: 20px;
}
.insurance-box {
width: 904px;
height: auto;
margin: 0 auto;
margin-top: 20px;
}
.dispute-box {
width: 904px;
height: auto;
margin: 0 auto;
margin-top: 20px;
}
.footer-box {
width: 904px;
height: auto;
margin: 0 auto;
margin-top: 20px;
text-indent: 2em;
margin-bottom: 20px;
font-size: 13px;
}
</style>
</head>
<body>
<div class="content">
<!-- 头部 -->
<div class="head-box">
<div class="head-top">
<div class="head-left">
<span class="title">平行进口汽车三包凭证预览</span>
</div>
<div class="head-right">
<div class="qrcode-box">
<img class="qrcode-img"
src="${vehicleQrCodeImage}"
alt="">
</div>
<div class="qrcode-txt-box">
<span class="txt">电子三包凭证</span>
</div>
</div>
</div>
<div class="head-bottom">
<!-- 分隔盒子 -->
<div class="separate-left"></div>
<div class="separate-right"></div>
</div>
</div>
<!-- 基本信息 -->
<div class="base-box">
<table border="1">
<tr class="bg-head-1">
<th colspan="2">
<span>基本信息</span>
</th>
</tr>
<tr>
<td colspan="2">
<span>三包凭证编号:${threeGuaranteesBasic.vin}</span>
</td>
</tr>
<tr>
<th colspan="2" class="bg-head-2">
<span class="color-3b3b3b">产品信息</span>
</th>
</tr>
<tr>
<td>
<span>产品品牌:${threeGuaranteesBasic.certificationModelBrand}</span>
</td>
<td>
<span>型号:${threeGuaranteesBasic.certificationModelNumber}</span>
</td>
</tr>
<tr>
<td style="border-bottom: none;">
<span>车辆类型:${threeGuaranteesBasic.vehicleType}</span>
</td>
<td style="border-bottom: none;">
<span>生产日期:${threeGuaranteesBasic.productionDate}</span>
</td>
</tr>
</table>
<table border="1">
<tr>
<th colspan="2" class="bg-head-2">
<span class="color-3b3b3b">生产者信息</span>
</th>
</tr>
<tr>
<td style="min-width: 450px;">
<span>名称:${threeGuaranteesBasic.proCompanyName}</span>
</td>
<td>
<span>邮政编码:${threeGuaranteesBasic.proPostalCode}</span>
</td>
</tr>
<tr>
<td>
<span>地址:${threeGuaranteesBasic.proBusinessAddress}</span>
</td>
<td>
<span>客服电话:${threeGuaranteesBasic.proCompanyPhone}</span>
</td>
</tr>
<tr>
<th colspan="2" class="bg-head-2">
<span class="color-3b3b3b">销售者信息</span>
</th>
</tr>
<tr>
<td>
<span>名称:${threeGuaranteesBasic.saleCompanyName}</span>
</td>
<td>
<span>邮政编码:${threeGuaranteesBasic.salePostalCode}</span>
</td>
</tr>
<tr>
<td style="border-bottom: none;">
<span>地址:${threeGuaranteesBasic.saleBusinessAddress}</span>
</td>
<td style="border-bottom: none;">
<span>客服电话:${threeGuaranteesBasic.saleCompanyPhone}</span>
</td>
</tr>
</table>
<table border="1">
<tr>
<th colspan="2" class="bg-head-2">
<span class="color-3b3b3b">修理者信息</span>
</th>
</tr>
<tr>
<td colspan="2">
<span>平行进口汽车三包保修网点信息查询:</span>
</td>
</tr>
<tr>
<td style="border-right: none;border-bottom:none;max-width: 500px;">
<div style="line-height: 30px;">
<span>方式一:中国汽车三包网:${threeGuaranteesBasic.threeGuaranteeNet}</span><br>
<span>方式二:平行进口汽车三包服务网:${threeGuaranteesBasic.threeGuaranteeServiceNet}</span><br>
<span>方式三:微信扫一扫右侧二维码</span>
</div>
</td>
<td style="border-left: none;border-bottom:none;margin: 0%;padding: 0%;text-align: right;">
<div style="width: auto;height: 164px;padding: 16px 10px 16px 0px;">
<img class="qrcode-img_"
src="${threeGuaranteesBasic.threeGuaranteeServiceQrCode}"
alt="">
</div>
</td>
</tr>
</table>
<table border="1">
<tr>
<th colspan="2" class="bg-head-2">
<span class="color-3b3b3b">交付信息</span>
</th>
</tr>
<tr>
<td style="border-bottom: none;">
<span>开具购车发票日期:${threeGuaranteesBasic.openInvoiceDate}</span>
</td>
<td style="border-bottom: none;">
<span>交付车辆日期:${threeGuaranteesBasic.deliverDate}</span>
</td>
</tr>
</table>
<table border="1">
<tr>
<th colspan="2" class="bg-head-2">
<span class="color-3b3b3b">三包条款</span>
</th>
</tr>
<tr>
<td colspan="2">
<span>汽车产品包修期:${qualityGuaranteeTermHtmlVo.guaranteePeriod}</span>
</td>
</tr>
<tr>
<td colspan="2">
<span>汽车产品三包有效期:${qualityGuaranteeTermHtmlVo.threeGuaranteesValidPeriod}</span>
</td>
</tr>
<tr>
<td colspan="2">
<span>退换车补偿系数及计算公式:${qualityGuaranteeTermHtmlVo.returnExchangeDescription}</span>
</td>
</tr>
<tr>
<td style="min-width: 200px;">
<div style="">
<span>其他三包责任承诺:</span>
</div>
</td>
<td style="">
<p style="font-size: 13px;line-height: 20px;">
${qualityGuaranteeTermHtmlVo.otherThreeGuaranteesPromise}
</p>
</td>
</tr>
<tr>
<td colspan="2">
销售者签章:
</td>
</tr>
</table>
</div>
<!-- 主要总成和系统的主要零部件种类范围 -->
<div class="main-box">
<table border="1" style="text-align: center;">
<tr class="bg-head-1">
<th colspan="2">
<span>主要总成和系统的主要零部件种类范围</span>
</th>
</tr>
<tr>
<th style="min-width: 200px;">
<span class="color-3b3b3b">总成系统</span>
</th>
<th>
<span class="color-3b3b3b">主要零部件种类范围</span>
</th>
</tr>
<#if qualityGuaranteeTermHtmlVo.mainVo?? && (qualityGuaranteeTermHtmlVo.mainVo?size > 0) >
<#list qualityGuaranteeTermHtmlVo.mainVo as item>
<tr>
<td>
${item.partKindSystem}
</td>
<td style="text-align: left;margin-left: 24px;">
<#assign index = 0>
<#if item.partKindRanges?exists>
<#list item.partKindRanges as obj>
<#if index != 0>,</#if>
${obj?trim}
<#assign index = index+1>
</#list>
</#if>
</td>
</tr>
</#list>
</#if>
</table>
</div>
<!-- 易损耗零部件种类范围及质量保证期 -->
<div class="easy-box">
<table border="1" style="text-align: center;">
<tr class="bg-head-1">
<th colspan="4">
<span>易损耗零部件种类范围及质量保证期</span>
<span style="font-size: 12px;">(易损件零部件种类范围以国标GB/T29632-2021为准)</span>
</th>
</tr>
<tr>
<th>
<span class="color-3b3b3b">易损耗零部件</span>
</th>
<th>
<span class="color-3b3b3b">质量保证期(以先到者为准)</span>
</th>
<th>
<span class="color-3b3b3b">易损耗零部件</span>
</th>
<th>
<span class="color-3b3b3b">质量保证期(以先到者为准)</span>
</th>
</tr>
<#if qualityGuaranteeTermHtmlVo.easyVos?? && (qualityGuaranteeTermHtmlVo.easyVos?size > 0) >
<#list qualityGuaranteeTermHtmlVo.easyVos as item>
<tr>
<td>
${item.k1}
</td>
<td>
${item.v1}
</td>
<td>
${item.k2}
</td>
<td>
${item.v2}
</td>
</tr>
</#list>
</#if>
</table>
</div>
<!-- 特殊零部件种类范围 -->
<div class="special-box">
<table border="1">
<tr class="bg-head-1">
<th>
<span>特殊零部件种类范围</span>
</th>
</tr>
<tr>
<th>
<span class="color-3b3b3b">需要根据车辆识别代码(VIN)等定制的特殊零部件种类范围</span>
</th>
</tr>
<tr>
<td>
<#assign index = 0>
<#if qualityGuaranteeTermHtmlVo.special?exists>
<#list qualityGuaranteeTermHtmlVo.special as item>
<#if index != 0>,</#if>
${item?trim}
<#assign index = index+1>
</#list>
</#if>
</td>
</tr>
</table>
</div>
<!-- 动力蓄电池容量衰减限值 -->
<div class="power-box">
<table border="1" style="text-align: center;">
<tr class="bg-head-1">
<th colspan="2">
<span>动力蓄电池容量衰减限值</span>
</th>
</tr>
<tr>
<th>
<span class="color-3b3b3b">需要根据车辆识别代码(VIN)等定制的特殊零部件种类范围</span>
</th>
<th>
<span class="color-3b3b3b">容量衰减限值</span>
</th>
</tr>
<#if qualityGuaranteeTermHtmlVo.batteryLossCustomVo?? && (qualityGuaranteeTermHtmlVo.batteryLossCustomVo?size > 0) >
<#list qualityGuaranteeTermHtmlVo.batteryLossCustomVo as item>
<tr>
<td style="text-align: left;margin-left: 24px;">
${item.period}
</td>
<td>
${item.attenuationLimit}
</td>
</tr>
</#list>
</#if>
<tr>
<td colspan="2" style="text-align: left;margin-left: 24px;">
<span>注:正常使用情况下,动力蓄电池Ah容量较额定容量的衰减不超过此表。</span>
</td>
</tr>
</table>
</div>
<!-- 平行进口汽车三包保险信息 -->
<div class="insurance-box">
<table border="1" style="text-align: center;">
<tr class="bg-head-1">
<th colspan="4">
<span>平行进口汽车三包保险信息</span>
</th>
</tr>
<tr>
<th>
<span class="color-3b3b3b">保险公司名称</span>
</th>
<th>
<span class="color-3b3b3b">保险产品名称</span>
</th>
<th>
<span class="color-3b3b3b">保单号</span>
</th>
<th>
<span class="color-3b3b3b">客服电话</span>
</th>
</tr>
<tr>
<td>
<span>${threeGuaranteesPolicy.companyName}</span>
</td>
<td>
<span>${threeGuaranteesPolicy.insuranceType}</span>
</td>
<td>
<span>${threeGuaranteesPolicy.policyNo}</span>
</td>
<td>
<span>${threeGuaranteesPolicy.insureHotLine}</span>
</td>
</tr>
</table>
</div>
<!-- 三包责任争议处理 -->
<div class="dispute-box">
<table border="1" style="text-align: center;">
<tr class="bg-head-1">
<th colspan="3">
<span>三包责任争议处理</span>
</th>
</tr>
<tr>
<th>
<span class="color-3b3b3b">三包责任争议处理机构</span>
</th>
<th>
<span class="color-3b3b3b">地址</span>
</th>
<th>
<span class="color-3b3b3b">咨询电话</span>
</th>
</tr>
<#if disputeInstitutions?? && (disputeInstitutions?size > 0) >
<#list disputeInstitutions as item>
<tr>
<td>
${item.name}
</td>
<td>
${item.address}
</td>
<td>
${item.phone}
</td>
</tr>
</#list>
</#if>
</table>
</div>
<div class="footer-box">
${promptOne}
</div>
</div>
</body>
</html>
3.效果图
4.代码实现
1.工具类
package com.yiliancar.process.third.utils;
import com.itextpdf.barcodes.Barcode128;
import com.itextpdf.barcodes.BarcodeQRCode;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.attach.ITagWorker;
import com.itextpdf.html2pdf.attach.ProcessorContext;
import com.itextpdf.html2pdf.attach.impl.DefaultTagWorkerFactory;
import com.itextpdf.html2pdf.attach.impl.tags.HtmlTagWorker;
import com.itextpdf.html2pdf.attach.impl.tags.ImgTagWorker;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.IPropertyContainer;
import com.itextpdf.layout.element.Image;
import com.itextpdf.layout.font.FontProvider;
import com.itextpdf.styledxmlparser.node.IElementNode;
import com.itextpdf.styledxmlparser.node.impl.jsoup.node.JsoupElementNode;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.FileCopyUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Objects;
/**
* @author zhanxuewei
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class PdfUtil {
private static final ConverterProperties CONVERTER_PROPERTIES;
private static final PageSize DEFAULT_LABEL_L10 = new PageSize(new Rectangle(0F, 0F, 775.00F, 1000.00F));
private static final PageSize A4 = PageSize.A4;
public static final String BARCODE_TYPE_BARCODE128 = "barcode128";
public static final String BARCODE_TYPE_QRBARCODE = "qrBarcode";
public static final String BARCODE_CODE_NUMBER_VALUE = "code";
static {
CONVERTER_PROPERTIES = new ConverterProperties();
CONVERTER_PROPERTIES.setTagWorkerFactory(new CustomTagWorkerFactory());
CONVERTER_PROPERTIES.setCharset("UTF-8");
FontProvider fontProvider = new FontProvider();
fontProvider.addStandardPdfFonts();
addFont(fontProvider, "pdf/simsun.ttf");
addFont(fontProvider, "pdf/simsunb.ttf");
addFont(fontProvider, "pdf/code128.ttf");
addFont(fontProvider, "pdf/simhei.ttf");
addFont(fontProvider, "pdf/GenJyuuGothicX-Medium.ttf");
addFont(fontProvider, "pdf/Microsoft_YaHei_UI_Bold.ttf");
addFont(fontProvider, "pdf/NotoSansCJKsc-Regular.otf");
}
/**
* Add font {@link FontProvider}
* @param fontProvider {@link FontProvider}
* @param fontFileName fontFileName
*/
private static void addFont(FontProvider fontProvider, String fontFileName) {
try (InputStream resourceAsStream = PdfUtil.class.getClassLoader().getResourceAsStream(fontFileName)) {
byte[] bytes = FileCopyUtils.copyToByteArray(resourceAsStream);
fontProvider.addFont(bytes, PdfEncodings.IDENTITY_H);
CONVERTER_PROPERTIES.setFontProvider(fontProvider);
} catch (IOException e) {
log.warn("读取字体文件出错:" + e.getMessage());
}
}
public static ByteArrayInputStream bytes2InputStream(byte[] bytes) {
return new ByteArrayInputStream(bytes);
}
public static byte[] merge(List<InputStream> inputStreams) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
PdfDocument newPdf = new PdfDocument(new PdfWriter(out));
for (InputStream inputStream : inputStreams) {
Objects.requireNonNull(inputStream);
PdfDocument pdfDocument = new PdfDocument(new PdfReader(inputStream));
pdfDocument.copyPagesTo(1, pdfDocument.getNumberOfPages(), newPdf);
}
newPdf.close();
return out.toByteArray();
} catch (Exception ex) {
log.error(ex.getMessage(), ex);
throw new RuntimeException("合并标签失败,系统内部错误. ", ex);
}
}
public static InputStream html2Pdf(String html) throws IOException {
return html2Pdf(html, CONVERTER_PROPERTIES, DEFAULT_LABEL_L10);
}
public static InputStream html2PdfA4(String html) throws IOException {
return html2Pdf(html, CONVERTER_PROPERTIES, A4);
}
public static InputStream html2Pdf(String html, ConverterProperties converterProperties, PageSize pageSize) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PdfWriter pdfWriter = new PdfWriter(out);
PdfDocument pdfDocument = new PdfDocument(pdfWriter);
pdfDocument.setDefaultPageSize(pageSize);
HtmlConverter.convertToPdf(html, pdfDocument, converterProperties);
return new ByteArrayInputStream(out.toByteArray());
}
}
class CustomHtmlTagWorker extends HtmlTagWorker {
public CustomHtmlTagWorker(IElementNode element, ProcessorContext context) {
super(element, context);
Document document = (Document) getElementResult();
document.setLeftMargin(1F);
document.setRightMargin(1F);
document.setTopMargin(1F);
document.setBottomMargin(1F);
}
public static CustomHtmlTagWorker newInstance(IElementNode element, ProcessorContext context) {
return new CustomHtmlTagWorker(element, context);
}
}
@Slf4j
class CustomTagWorkerFactory extends DefaultTagWorkerFactory {
@Override
public ITagWorker getCustomTagWorker(IElementNode tag, ProcessorContext context) {
if (tag instanceof JsoupElementNode) {
JsoupElementNode tag1 = (JsoupElementNode) tag;
String name = tag1.name();
if ("html".equals(name)) {
return CustomHtmlTagWorker.newInstance(tag, context);
}
}
if ("img".equals(tag.name())) {
String codeType = tag.getAttribute("codeType");
log.info("getAttribute:{}", codeType);
if (PdfUtil.BARCODE_TYPE_QRBARCODE.equals(codeType)) {
return new QRCodeTagWorker(tag, context);
}
if (PdfUtil.BARCODE_TYPE_BARCODE128.equals(codeType)) {
return new Barcode128Work(tag, context);
}
}
return super.getCustomTagWorker(tag, context);
}
}
class Barcode128Work extends ImgTagWorker {
Image image;
/**
* Creates a new {@link ImgTagWorker} instance.
*
* @param element the element
* @param context the context
*/
public Barcode128Work(IElementNode element, ProcessorContext context) {
super(element, context);
}
@Override
public void processEnd(IElementNode element, ProcessorContext context) {
Barcode128 barcode128 = new Barcode128(context.getPdfDocument());
String code = element.getAttribute(PdfUtil.BARCODE_CODE_NUMBER_VALUE);
barcode128.setCode(code);
PdfFormXObject formObject = barcode128.createFormXObject(context.getPdfDocument());
image = new Image(formObject);
}
@Override
public IPropertyContainer getElementResult() {
return image;
}
}
@Slf4j
class QRCodeTagWorker extends ImgTagWorker {
private BarcodeQRCode qrCode;
private Image qrCodeAsImage;
public QRCodeTagWorker(IElementNode element, ProcessorContext context) {
super(element, context);
}
@Override
public void processEnd(IElementNode element, ProcessorContext context) {
String code = element.getAttribute(PdfUtil.BARCODE_CODE_NUMBER_VALUE);
qrCode = new BarcodeQRCode(code);
//Transform barcode into image
qrCodeAsImage = new Image(qrCode.createFormXObject(context.getPdfDocument()));
}
@Override
public IPropertyContainer getElementResult() {
return qrCodeAsImage;
}
}
核心业务代码
private final FreeMakerHelper freeMakerHelper;
String html = freeMakerHelper.process("certificate.html", threePackagesCredentialsHtmlVO);
InputStream inputStream = PdfUtil.html2Pdf(html);