一、前提
1,做一个能动态改变样式的pdf,并且将文本内容填充进去,那么使用PdfRender就做不到了,e签宝的模板接口也做不到动态改变字体的颜色等。百度查到可以使用itext的html2pdf,可是却没想到在使用过程中有那么多坑,而且很多教程都不贴html,所说html规范严格也没说到底咋严格,最终还是跟源码也解决问题。
2,最终需要的pdf样式如下:
一些信息我马赛克了,可以看到标题是优设标题黑,正文是微软雅黑,部分字体要根据所填内容不同变换颜色
二、代码
<dependency> <groupId>com.itextpdf</groupId> <artifactId>kernel</artifactId> <version>8.0.2</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>io</artifactId> <version>8.0.2</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>layout</artifactId> <version>8.0.2</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>forms</artifactId> <version>8.0.2</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>pdfa</artifactId> <version>8.0.2</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>font-asian</artifactId> <version>8.0.2</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.18</version> </dependency> <!--itext7 html转pdf用到的包--> <dependency> <groupId>com.itextpdf</groupId> <artifactId>html2pdf</artifactId> <version>5.0.2</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.31</version> </dependency>maven引入
package com.cfam.util; import com.itextpdf.html2pdf.ConverterProperties; import com.itextpdf.html2pdf.HtmlConverter; import com.itextpdf.html2pdf.css.apply.impl.DefaultCssApplierFactory; import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider; import com.itextpdf.io.font.FontProgramFactory; import com.itextpdf.io.font.PdfEncodings; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.kernel.geom.PageSize; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.layout.Document; import com.itextpdf.layout.font.FontProvider; import com.itextpdf.styledxmlparser.css.media.MediaDeviceDescription; import com.itextpdf.styledxmlparser.css.media.MediaType; import com.itextpdf.text.DocumentException; import freemarker.cache.FileTemplateLoader; import freemarker.cache.TemplateLoader; import freemarker.template.Configuration; import freemarker.template.Template; import java.io.*; import java.util.HashMap; import java.util.Map; /** * html 填充数据渲染 转 pdf */ public class PdfGeneratorTest { /** * 使用Freemarker引擎加载HTML模板文件并填充变量值,并将HTML字符串转换为PDF文件 * * @param data 模板要填充的数据 * @throws Exception * @return */ public static String generatePDF(Map<String, Object> data, String templateDir, String templateName, String pdfPath, String fileName) throws Exception { // 使用Freemarker引擎加载HTML模板文件并填充变量值 TemplateLoader templateLoader = new FileTemplateLoader(new File(templateDir)); Configuration cfg = new Configuration(Configuration.getVersion()); cfg.setTemplateLoader(templateLoader); Template template = cfg.getTemplate(templateName,"UTF-8"); StringWriter out = new StringWriter(); template.process(data, out); out.flush(); String htmlContent = out.toString(); return convertHtmlToPdf(htmlContent, pdfPath, fileName); } /** * 使用iText 7将HTML字符串转换为PDF文件,并返回PDF文件的二进制数据 * * @param htmlString 待转换的HTML字符串 * @return 返回生成的PDF文件内容 * @throws IOException */ private static String convertHtmlToPdf(String htmlString, String path, String fileName) throws IOException, DocumentException { File compressedImageFile = new File(path, fileName); OutputStream os = new FileOutputStream(compressedImageFile); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PdfWriter writer = new PdfWriter(outputStream); PdfDocument pdf = new PdfDocument(writer); Document document = new Document(pdf, new PageSize(600.0F, 1150.0F)); // 设置左、右、上、下四个边距的值,以点(pt)为单位 document.setMargins(0, 0, 0, 0); // 设置中文字体 FontProvider fontProvider = new DefaultFontProvider(false, false, false); //添加自定义字体,例如微软雅黑 PdfFont microsoft = PdfFontFactory.createFont(FontProgramFactory.createFont("C:\\Users\\carcredit\\Desktop\\youshebiaotihei.ttf")); fontProvider.addFont(microsoft.getFontProgram(), PdfEncodings.IDENTITY_H); PdfFont microsoft1 = PdfFontFactory.createFont(FontProgramFactory.createFont("C:\\Users\\carcredit\\Desktop\\microsoftyahei.ttf")); fontProvider.addFont(microsoft1.getFontProgram(), PdfEncodings.IDENTITY_H); ConverterProperties converterProps = new ConverterProperties(); converterProps.setFontProvider(fontProvider); // 调用HtmlConverter类的convertToPdf函数,将HTML字符串转换为PDF文件 converterProps.setMediaDeviceDescription(new MediaDeviceDescription(MediaType.PRINT)); converterProps.setCssApplierFactory(new DefaultCssApplierFactory()); HtmlConverter.convertToPdf(htmlString, pdf, converterProps); pdf.close(); // 将PDF文件转换为字节数组并返回 outputStream.writeTo(os); return compressedImageFile.getPath(); } public static void main(String[] args) throws Exception { Map map = new HashMap(); map.put("name", "太白金星"); map.put("gender", "男"); map.put("nationality", "仙族"); map.put("address", "天庭"); map.put("td_3_month_loan_dw","0"); map.put("bairong_network_time_dw","[24,+)"); map.put("fahai_ajlc_count","0"); map.put("fahai_fygg_count","0"); map.put("fahai_ktgg_count","0"); map.put("fahai_zxgg_count","0"); map.put("fahai_cpws_count","0"); map.put("pboc_edu_info_dw","武神"); map.put("arp_overdue_055_interval","(3,5]"); map.put("arp_account_028_interval","0"); map.put("arp_account_018_interval","--"); map.put("arp_account_012_interval","(0,30000]"); map.put("arp_overdue_105_interval",">20"); map.put("arp_base_006","2024-01-17 17:18"); map.put("apply_con_approve_status_name_dw","建议拒绝"); map.put("pre_pboc_overdue_max_month_dw","(5,7]"); map.put("pboc_overdue_max_amount_dw","(20000,40000]"); map.put("apply_con_reject_reason_sales_dw","拒绝:"); map.put("pboc_partner_name","占几个"); map.put("apply_con_result_hf","--"); map.put("score_level_up","B"); map.put("pre_apply_con_cert_name","李长庚"); map.put("pre_pboc_belonger_marital_status","已婚"); map.put("pre_cert_no","156956265498563654"); map.put("pboc_phone_no_lastest","17765571433"); map.put("pboc_partner_phone","15965395626"); map.put("pboc_partner_cert_no","163956956485236542"); map.put("mobile_phone_operator","移动"); map.put("pboc_house_loan_exists","否"); map.put("pboc_phone_compare","一致"); map.put("pboc_monthly_pboc_repayments_interval","(30000,50000]"); map.put("pre_phone","13890786915"); map.put("bairong_bankfour_res","一致"); map.put("pboc_car_loan_exists","是"); map.put("pboc_total_credit_line_credit_card_interval","(0,30000]"); map.put("pboc_utilization_rate_used_quota_interval","0"); map.put("pboc_query_count_1_month_interval","0"); map.put("pboc_query_count_3_months_interval","0"); map.put("create_time","2024/1/18 15:12"); map.put("update_time","12:58.3"); map.put("create_user","0"); map.put("update_user","0"); map.put("sync_time","2024/1/18 15:12"); generatePDF(map, "C:\\Users\\carcredit\\Desktop", "boke.html", "C:\\Users\\carcredit\\Desktop", "测试程序生成的.pdf"); } }PdfGeneratorTest.java
<!DOCTYPE html> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <head> <title> </title> </head> <style type="text/css"> p { margin: 0px; } .report { width: 679px; margin: 0 auto; font-family: 'microsoft yahei'; color: #333333; font-size: 16px; } .head { height: 80px; line-height: 80px; background: #4384FE; color: #fff; /* text-align: center; */ overflow: hidden; box-sizing: border-box; } .head .main_head { font-family: "youshebiaotihei"; margin-top: -10px; font-size: 39px; } .head span { display: inline-block; } .head .report_time { height: 100%; padding-top: 20px; padding-right: 15px; box-sizing: border-box; font-size: 12px; } .small_head { margin-top: 18px; height: 20px; line-height: 14px; } .small_head .treetop { display: block; font-style: normal; width: 3px; height: 16px; background: #4384FE; margin-right: 5px; float: left; } .small_head span { display: block; height: 16px; } .bg { background: #F8FAFC; margin-top: 9px; } .custom_content { height: 102px; } .custom_content .first_storey { height: 100%; padding-top: 14px; padding-left: 9px; box-sizing: border-box; } .custom_content .second_storey { height: 100%; padding-top: 14px; } .custom_content .second_storey img { width: 155px; height: 41px; margin-right: -4px; } .examine_content { height: 87px; } .examine_content div { width: 50%; } .examine_content .left_01 { margin-top: 14px; padding-left: 9px; box-sizing: border-box; } .examine_content .left_01 span { display: block; } .examine_content .left_01 span:nth-child(2) { margin-top: 24px; } .examine_content .right_01 { line-height: 87px; } .examine_content .right_01 span { display: inline-block; color: #08CA85; font-size: 25px; } .unified-format { height: 40px; } .unified-format .left_01 { margin-top: 16px; padding-left: 10px; box-sizing: border-box; } .unified-format .left_01 span { display: block; width: 33%; height: 40px; line-height: 40px; } .judicial-content { height: 70px; margin: 0 auto; margin-top: 9px; padding-top: 8px; padding-left: 8px; box-sizing: border-box; } .judicial-content div { float: left; } .judicial-content div span:nth-child(1) { display: block; width: 130px; height: 26px; line-height: 26px; border-radius: 2px; color: #fff; background: linear-gradient(180deg, #7FA1FF 0%, #4384FE 100%); margin-right: 4px; text-align: center; } .judicial-content div span:nth-child(2) { display: block; width: 100%; text-align: center; margin: 9px 0px; } .phone-content { height: 70px; margin: 0 auto; margin-top: 9px; padding-top: 8px; padding-left: 8px; box-sizing: border-box; } .phone-content div { float: left; } .phone-content div span:nth-child(1) { display: block; width: 164px; height: 26px; line-height: 26px; border-radius: 2px; color: #fff; background: linear-gradient(180deg, #7FA1FF 0%, #4384FE 100%); margin-right: 3px; text-align: center; } .phone-content div span:nth-child(2) { display: block; width: 100%; text-align: center; margin: 9px 0px; } .bull-content { height: 44px; padding: 9px; margin: 0 auto; margin-top: 9px; box-sizing: border-box; } .bull-content div span { display: block; width: 330px; height: 26px; line-height: 26px; background: linear-gradient(180deg, #7FA1FF 0%, #4384FE 100%); border-radius: 2px; text-align: center; color: #fff; } .bull-content div span:nth-child(2) { background: #DEE9FF; color: #333333; } .info-content { height: 71px; padding: 9px; box-sizing: border-box; } .info-content div { width: 50%; height: 100%; } .info-content div span { display: block; } .info-content div span:nth-child(1) { width: 330px; height: 26px; line-height: 26px; background: linear-gradient(180deg, #7FA1FF 0%, #4384FE 100%); border-radius: 2px; color: #fff; text-align: center; } .info-content div span:nth-child(2) { text-align: center; margin-top: 10px; } .loan-content { height: 265px; overflow: hidden; box-sizing: border-box; } .loan-content .loan_storey { padding-top: 9px; padding-left: 9px; } .loan-content .content { width: 50%; } .loan-content .content p:nth-child(1) { width: 330px; height: 26px; line-height: 26px; background: linear-gradient(180deg, #7FA1FF 0%, #4384FE 100%); border-radius: 2px; color: #fff; text-align: center; } .loan-content .content p:nth-child(2) { height: 58px; line-height: 58px; text-align: center; /* margin-top: 18px; margin-bottom: 21px; */ } .query-content { height: 71px; padding: 9px; box-sizing: border-box; } .query-content div { width: 50%; height: 100%; } .query-content div span { display: block; } .query-content div span:nth-child(1) { width: 330px; height: 26px; line-height: 26px; background: linear-gradient(180deg, #7FA1FF 0%, #4384FE 100%); border-radius: 2px; color: #fff; text-align: center; } .query-content div span:nth-child(2) { text-align: center; margin-top: 9px; } .m24 { margin-bottom: 21px; } </style> <body> <div class="report"> <div class="container"> <div class="head"> <span class="main_head" style="padding-left: 120px;float: left;">个人综合风险评估报告</span> <!-- <span class="report_time" style="float:left;">报告时间:${arp_base_006}</span> --> <#if arp_base_006?has_content> <span class="report_time" style="float:left;">报告时间:${arp_base_006}</span> <#else> <span class="report_time" style="float:left;">报告时间:--</span> </#if> </div> <p class="small_head"><span class="treetop"></span><span>客户信息</span></p> <div class="custom_content bg"> <div class="first_storey" style="float:left; width: 50%; box-sizing: border-box;"> <p>姓名:${pre_apply_con_cert_name}</p> <p style="margin-top: 26px;">身份证号 :${pre_cert_no} </p> </div> <div class="second_storey" style="float:right; width: 50%; box-sizing: border-box;"> <p>客户风险评级 :</p> <div style="height: 41px; line-height: 41px; margin-top: 14px;"> <p style="float:left;"> <#if (score_level_up=="A" )> <img src="https://www.carcredit.com.cn/riskreporttemplate/A.png" alt="A"> <#elseif (score_level_up=="B" )> <img src="https://www.carcredit.com.cn/riskreporttemplate/B.png" alt="B"> <#elseif (score_level_up=="C" )> <img src="https://www.carcredit.com.cn/riskreporttemplate/C.png" alt="C"> <#elseif (score_level_up=="D" )> <img src="https://www.carcredit.com.cn/riskreporttemplate/D.png" alt="D"> </#if> </p> <p style="float:left; margin-left: 14px;"> <span>信用评级为:</span> <#if (score_level_up=="A" )> <span style="color: #03AA3E;">${score_level_up}</span> <#elseif (score_level_up=="B" )> <span style="color: #77D33D;">${score_level_up}</span> <#elseif (score_level_up=="C" )> <span style="color: #FFB84C;">${score_level_up}</span> <#elseif (score_level_up=="D" )> <span style="color: #FE3A41;">${score_level_up}</span> <#else> <span>--</span> </#if> </p> </div> </div> </div> <p class="small_head"><span class="treetop"></span><span>审核结果</span></p> <div class="examine_content bg"> <div class="left_01" style="float: left;"> <span>通过资方:${apply_con_result_hf} </span> <span>拒绝原因:${apply_con_reject_reason_sales_dw} </span> </div> <div class="right_01" style="float:right;"> <#if (apply_con_approve_status_name_dw=="建议通过" )> <span style="color: #08CA85;">${apply_con_approve_status_name_dw}</span> <#elseif (apply_con_approve_status_name_dw=="建议拒绝" )> <span style="color: #E72B49;">${apply_con_approve_status_name_dw}</span> <#else> <span>--</span> </#if> </div> </div> <p class="small_head"><span class="treetop"></span><span>身份信息</span></p> <div class="unified-format bg"> <div class="left_01"> <span style="float: left;">婚姻状况:${pre_pboc_belonger_marital_status} </span> <span style="float: left; text-align: center;">学历:${pboc_edu_info_dw}</span> <span style="float: left; text-align: right;">手机号:${pboc_phone_no_lastest}</span> </div> </div> <p class="small_head"><span class="treetop"></span><span>司法信息</span></p> <div class="judicial-content bg"> <div> <span>裁判文书</span> <span>${fahai_cpws_count}</span> </div> <div> <span>执行公告</span> <span>${fahai_zxgg_count}</span> </div> <div> <span>开庭公告</span> <span>${fahai_ktgg_count}</span> </div> <div> <span>法院公告</span> <span>${fahai_fygg_count}</span> </div> <div> <span>案件流程</span> <span>${fahai_ajlc_count}</span> </div> </div> <p class="small_head"><span class="treetop"></span><span>手机号验证</span></p> <div class="phone-content bg"> <div> <span>手机号码</span> <span>${pre_phone}</span> </div> <div> <span>运营商名称</span> <#if (mobile_phone_operator="移动" )> <span style="color: #3062EC;">${mobile_phone_operator}</span> <#elseif (mobile_phone_operator="联通" )> <span style="color: #FFA922;">${mobile_phone_operator}</span> <#elseif (mobile_phone_operator="电信" )> <span style="color: #E40C2F;">${mobile_phone_operator}</span> <#else> <span>--</span> </#if> <!-- <span>${mobile_phone_operator}</span> --> </div> <div> <span>验证结果</span> <span>${bairong_bankfour_res}</span> </div> <div> <span>在网时长</span> <span>${bairong_network_time_dw}</span> </div> </div> <p class="small_head"><span class="treetop"></span><span>多头查询</span></p> <div class="bull-content bg"> <div> <span style="float: left;">3个月内申请人在多个平台申请借款</span> <span style="float: right;">${td_3_month_loan_dw}</span> </div> </div> <p class="small_head"><span class="treetop"></span><span>信息提示</span></p> <div class="info-content bg"> <div style="float: left;"> <span>月负债</span> <span>${pboc_monthly_pboc_repayments_interval}</span> </div> <div style="float: right;"> <span>手机号比对不一致</span> <span>${pboc_phone_compare}</span> </div> </div> <p class="small_head"><span class="treetop"></span><span>贷款信息汇总</span></p> <div class="loan-content bg"> <div class="loan_storey"> <div class="content" style="float: left;"> <p>贷款账户数</p> <p>${arp_overdue_105_interval}</p> </div> <div class="content" style="float: left;"> <p>未结清贷款余额</p> <p>${arp_account_012_interval}</p> </div> </div> <div class="loan_storey"> <div class="content" style="float: left;"> <p>历史是否有房贷</p> <p>${pboc_house_loan_exists}</p> </div> <div class="content" style="float: left;"> <p>未结清房贷贷款总额</p> <p>${arp_account_018_interval}</p> </div> </div> <div class="loan_storey"> <div class="content" style="float: left;"> <p>历史是否有车贷</p> <p>${pboc_car_loan_exists}</p> </div> <div class="content" style="float: left;"> <p>未结清个人汽车贷款总额</p> <p>${arp_account_028_interval}</p> </div> </div> </div> <p class="small_head"><span class="treetop"></span><span>查询信息汇总</span></p> <div class="query-content bg"> <div style="float: left;"> <span>客户近1个月查询次数 </span> <span>${pboc_query_count_1_month_interval}</span> </div> <div style="float: right;"> <span>客户近3个月查询次数</span> <span>${pboc_query_count_3_months_interval}</span> </div> </div> <p class="small_head"><span class="treetop"></span><span>配偶信息</span></p> <div class="unified-format bg m24"> <div class="left_01"> <span style="float:left;">姓名:${pboc_partner_name}</span> <span style="float:left; text-align: center;">证件号码:${pboc_partner_phone}</span> <span style="float: left; text-align: right;">联系电话:${pboc_partner_phone}</span> </div> </div> </div> </div> </body> </html>boke.html
用到的字体放在百度网盘了:
链接:https://pan.baidu.com/s/1O7sqPnWOHTd1ftruB-pYnQ
提取码:azxq
三、一些注意事项
1,加载html模板的方式
① 加载本地模板(不是放在程序resources下,而是服务器的一个位置)
② 加载url模板
2,html中的css格式不生效
①首先html的格式一定要标准,标签结尾一定要有,比如<div>开头,必须有</div>
②检查引入的itext是否是最新版本,我一开始引入的是5.0.2,结果css的渐变背景(linear-gradient)标签就没有生效,查看maven仓库发现最新的到8.0.2了,html2pdf升到5.0.2,freeMaker到2.3.31
③建议使用Visual Studio Code格式化一下html,方便检查格式
3,html填充内容的velocity语法
如果用过idea的easyCode插件那一定对velocity不陌生,关于velocity的语法可以参考以下文档:Java中Velocity ,freeMaker使用的稍微有点区别:模板语言参考 - FreeMarker 中文官方参考手册 (foofun.cn)
① 一般填充使用 ${}的方式,如
② if else
一般在easyCode模板中是这样使用的
但在这里的html中,应该这样使用
例如:
或者
其余用法请参考上面文档
4,字体不生效 font-family的问题
首先不用系统自带的字体,或者网上下载的乱七八糟的漂亮字体(有些会乱码)
参考的链接如下:iText7高级教程之html2pdf——6.在pdfHTML中使用字体
有两种方式加载字体, 第一种方式,在html中指定@font-face 从网络下载字体,第二种方式,在后台指定字体,两种方式都需要在html样式中指定“font-family”(注:font-family后一定要加单引号);
① html中指定font-face,如下图
此时后台就可以不加字体或加一个默认的字体。(注意,此时两条线的两端连接处设置的名称一定要一致,下面会说匹配字体的事儿,这里设置 @font-face中的font-family的名字等于给此字体设置了一个别名alias)
但是使用这个html生成的pdf有些文字字体不对,没有使用指定的,我一直以为是font-family没有生效,跟源码才发现是这个woff不支持某些字符,所以一定要先验证这字体好不好用啊,真坑,下面有验证方法。
② 后台指定字体
html中去掉@font-face, 只写font-family与后台加载的字体对应,比如
那么html中的font-family和后台加载的字体是如何对应的呢? 由于之前生成的字体都是混乱的,网上也找不到答案,贴点儿边的就说ttf的名字要是英文的,最后无可奈何跟了一下源码,才发现在layout包的字体选择器fontSelector中,是这样判断的
所以与ttf是否是英文也没关系,html中的font-family要与后台加载ttf的 familyNameLowerCase 相同,html中也不用写@font-face了,那么如何知道ttf的familyNameLowerCase呢?如下:
匹配familyNameLowerCase的过程其实是将所有的字体排序的过程,将匹配到的放在第一位,接下来还会继续循环排好序的字体集合逐字校验是否支持,如下:
所以这里要保证,加载的字体一定要支持设置了font-family标签的文字
如何验证是否支持某字呢?使用fontTools如下:
from fontTools.ttLib import TTFont ch = '姓' font_path = 'C:\\Users\\carcredit\\Desktop\\ef6d508.woff' font = TTFont(font_path) glyph_name = None for table in font['cmap'].tables: glyph_name = table.cmap.get(ord(ch)) if glyph_name is not None: break if glyph_name is not None: glyf = font['glyf'] found = glyf.has_key(glyph_name) and glyf[glyph_name].numberOfContours > 0 else: found = False print(found)校验字体是否支持某个字
可以看到截图中的woff文件,支持“姓”,却不支持“长”。
总结:如果html中没有指定font-family,那么最终会使用后台addFont的第一个字体;如果指定了font-family,但是名称与后台字体的familyNameLowerCase不同,那么最终会使用后台addFont的第一个字体(或者使用系统自带的默认字体或乱码);
但是要注意,后台加载的ttf越多,生成pdf的时间越慢,所以最好不要加载系统字体,即设置:
FontProvider fontProvider = new DefaultFontProvider(false, false, false)
由于fontSelector会将font-family跟每一个ttf作对比,所以如果想更快的生成pdf,应调整ttf的加载顺序,将pdf中使用次数较多的字体放在第一个,这样能确保第一次循环就匹配到;
出现字体混乱,第一时间验证字体文件是否支持。
5,生成的pdf出现偏移、缺失、分页
一开始设置的pdf纸张大小是A4,与html中定义的大小不同,那就会出现一部分被截断,或分页的情况(我的需求要不分页),所以要根据html的大小进行调试,直到合适
标签:map,java,pboc,height,content,itext,put,font,html2pdf From: https://www.cnblogs.com/hsql/p/17980241