<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.xhtmlrenderergroupId>
<artifactId>flying-saucer-pdfartifactId>
<version>9.1.6version>
dependency>
<dependency>
<groupId>ognlgroupId>
<artifactId>ognlartifactId>
<version>3.1.12version>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>2.5version>
dependency>
<dependency>
<groupId>org.jsoupgroupId>
<artifactId>jsoupartifactId>
<version>1.7.1version>
dependency>
根据模板生成文件,可以在模板里指定格式,在 resources/templtes目录下创建一个模板
htmlTemplate.html文件就是我的模板,font/SIMSUN.TTC文件解决转pdf是解决中文的字体,后面需要引入。
编写模板htmlTemplate.html
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<style type="text/css">
body {
font-family: SimSun;
}
.content_wrap {
padding: 20px 45px;
background: white;
-webkit-print-color-adjust: exact;
border-radius: 4px;
}
._header {
position: relative;
font-size: 18px;
line-height: 1;
color: black;
}
._header::after {
position: absolute;
left: -15px;
top: -10%;
transform: translateY(-50%);
content: "";
display: block;
width: 5px;
height: 20px;
background-color: #00bba6;
-webkit-print-color-adjust: exact;
}
.infos {
margin: 10px 0 30px 0;
padding: 20px 0px;
}
.infos span {
width: 33%;
margin-top: 20px;
margin-right: 10px;
}
._info-label {
font-size: 14px;
font-weight: 400;
color: #8E97A5;
line-height: 14px;
margin-right: 10px;
}
._info-value {
font-size: 14px;
font-weight: 400;
color: #2A3245;
line-height: 14px;
}
._table {
margin-top: 20px;
border: 1px solid #E1E6EC;
border-collapse: collapse;
}
._table tr {
border-bottom: 1px solid #E1E6EC;
height: 48px;
}
._table ._tr-val {
height: 60px;
}
td {
border-top: 0;
border-right: 1px #E1E6EC solid;
border-bottom: 1px #E1E6EC solid;
border-left: 0;
text-align: right;
}
table {
border-top: 1px #E1E6EC solid;
border-right: 0;
border-bottom: 0;
border-left: 1px #E1E6EC solid;
}
._table td {
text-align: right;
padding-right: 10px;
font-weight: bold;
font-size: 14px;
color: #2A3245;
border-right: 1px solid #E1E6EC;
}
._table .t-tithle {
background: #F3F6F9;
-webkit-print-color-adjust: exact;
}
._foot p {
margin-top: 20px;
}
._foot ._label {
font-size: 14px;
font-weight: 400;
color: #5F677A;
line-height: 14px;
margin-right: 45px;
}
._foot ._value {
font-size: 20px;
font-weight: bold;
color: #2A3245;
line-height: 20px;
}
style>
head>
<body>
<div class="detail-content content_wrap pl30" th:utext="${content}">
div>
body>
html>
package com.cqbay.maserb.factory;
import com.lowagie.text.BadElementException;
import com.lowagie.text.Image;
import com.lowagie.text.pdf.codec.Base64;
import org.w3c.dom.Element;
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 java.io.IOException;
/**
* @ClassName: Base64ImgReplacedElementFactory
* @Description: TODO
* @Author: Jane
* @Date: 2020/7/8 14:07
* @Version: V1.0
**/
public class Base64ImgReplacedElementFactory implements ReplacedElementFactory {
/**
* * 实现createReplacedElement 替换html中的Img标签
* *
* * @param c 上下文
* * @param box 盒子
* * @param uac 回调
* * @param cssWidth css宽
* * @param cssHeight css高
* * @return ReplacedElement
*
*/
@Override
public ReplacedElement createReplacedElement(LayoutContext c, BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) {
Element e = box.getElement();
if (e == null) {
return null;
}
String nodeName = e.getNodeName();
// 找到img标签
if (nodeName.equals("img")) {
String attribute = e.getAttribute("src");
FSImage fsImage;
try {
// 生成itext图像
fsImage = buildImage(attribute, uac);
} catch (BadElementException e1) {
fsImage = null;
} catch (IOException e1) {
fsImage = null;
}
if (fsImage != null) {
// 对图像进行缩放
if (cssWidth != -1 || cssHeight != -1) {
fsImage.scale(cssWidth, cssHeight);
}
return new ITextImageElement(fsImage);
}
}
return null;
}
/**
* * 编解码base64并生成itext图像
*
*/
protected FSImage buildImage(String srcAttr, UserAgentCallback uac) throws IOException,
BadElementException {
FSImage fiImg = null;
//图片的src要为src="https://img-blog.csdnimg.cn/2022010616555048137.jpg"这种base64格式
if (srcAttr.toLowerCase().startsWith("data:image/")) {
String base64Code = srcAttr.substring(srcAttr.indexOf("base64,") + "base64,".length(), srcAttr.length());
// 解码
byte[] decodedBytes = Base64.decode(base64Code);
fiImg = new ITextFSImage(Image.getInstance(decodedBytes));
} else {
fiImg = uac.getImageResource(srcAttr).getImage();
}
return fiImg;
}
@Override
public void reset() {
}
@Override
public void remove(Element arg0) {
}
@Override
public void setFormSubmissionListener(FormSubmissionListener arg0) {
}
}
PdfUtils
package com.cqbay.maserb;
import com.lowagie.text.DocumentException;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import com.cqbay.maserb.factory.Base64ImgReplacedElementFactory
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @ClassName: PdfUtils
* @Description: pdf工具类
* @Author: Jane
* @Date: 2020/7/8 14:27
**/
@Slf4j
public class PdfUtils {
/**
* 字体路径
*/
public static String FONT_PATH = "font/SIMSUN.TTC";
/**
* html模板路径
*/
public static final String HTML_TEMPLATE_PATH = "templates/pdf/htmlTemplate.html";
/**
* 生成的pdf保存目录,这里是固定了目录,文件名需自己拼装
*/
public static final String PDF_BASE_PATH = "src/main/resources/pdf/";
/**
* PDF扩展名
*/
public static final String PDF_EXTENSION = ".pdf";
/**
* 图片基础URL
*/
public static final String IMG_SAVE_URL = "http://117.78.37.58:8989/files/";
/**
* 图片保存路径
*/
public static final String IMG_SAVE_PATH = "src/main/resources/img/";
public static TemplateEngine templateEngine = new TemplateEngine();
/**
* 读取文件
*
* @param file
* @return 读取文件的内容
*/
public static String getFileString(File file) {
log.info("开始读取文件");
BufferedReader reader = null;
StringBuffer sb = new StringBuffer();
try {
reader = new BufferedReader(new FileReader(file));
String tempStr;
while ((tempStr = reader.readLine()) != null) {
sb.append(tempStr);
}
reader.close();
log.info("读取文件完成,文件信息:file={}", sb.toString());
return sb.toString();
} catch (IOException e) {
log.error("文件读取失败,cause--->" + e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
return sb.toString();
}
/**
* 根据模板生成html文件
*
* @param htmlTemplatePath html模板
* @param content 填充内容
* @return 根据模板生成的html
* @throws IOException
*/
public static String getHtml(String htmlTemplatePath, String content) {
if (StringUtils.isEmpty(htmlTemplatePath)) {
htmlTemplatePath = HTML_TEMPLATE_PATH;
}
if (StringUtils.isEmpty(content)) {
log.debug("生成html失败:内容content为未空");
return null;
}
try {
log.info("开始是生成html文件");
Resource resource = new ClassPathResource(htmlTemplatePath);
File sourceFile = resource.getFile();
Context context = new Context();
// 将内容写入模板
Map<String, Object> params = new HashMap<>();
params.put("content", content);
context.setVariables(params);
return templateEngine.process(getFileString(sourceFile), context);
} catch (IOException e) {
log.error("html文件生成失败:cause--->" + e.getMessage());
return null;
}
}
/**
* 根据html生成PDF
*
* @param html html内容
* @param file 输出pdf文件的路径
* @throws DocumentException
* @throws IOException
*/
public static void htmlToPdf(String html, File file) {
/**
* 切记 css 要定义在head 里,否则解析失败
* css 要定义字体
* 例如宋体style="font-family:SimSun"用simsun.ttc
*/
if (!file.exists()) {
try {
if (file.getParentFile() != null && !file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
log.info("开始根据html生成pdf,html={}", html);
OutputStream out = null;
try {
out = new FileOutputStream(file);
ITextRenderer renderer = new ITextRenderer();
// 携带图片,将图片标签转换为itext自己的图片对象
renderer.getSharedContext().setReplacedElementFactory(new Base64ImgReplacedElementFactory());
renderer.getSharedContext().getTextRenderer().setSmoothingThreshold(0);
// 解决中文支持问题
ITextFontResolver fontResolver = renderer.getFontResolver();
// 字体名称要大写,否则可能找不到
fontResolver.addFont(FONT_PATH, "Identity-H", false);
renderer.setDocumentFromString(html);
// 如果是本地图片使用 file:,这里指定图片的父级目录。html上写相对路径,
// renderer.getSharedContext().setBaseURL("file:/E:/img/")
// 处理图片
renderer.getSharedContext().setBaseURL(IMG_SAVE_URL);
renderer.layout();
renderer.createPDF(out);
out.flush();
log.info("pdf生成成功");
} catch (DocumentException e) {
log.error("pdf生成失败,cause--->" + e.getMessage());
} catch (IOException e) {
log.error("pdf生成失败,cause--->" + e.getMessage());
} finally {
try {
if (null != out) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 替换http图片url为其相对url
* 例如:http://117.78.37.58:8989/files/20200327\png\dcb09254b0d049d28550a9f31d8e88af.png
* 替换成:20200327/png/dcb09254b0d049d28550a9f31d8e88af.png
* 注意:一定要把 url中的所有 "\" 替换成 "/" 否者图片可能不会显示,原因不明
*
* @param html
* @return html字符串
*/
public static String replaceImgTagSrc(String html) {
log.info("开始替换图片");
if (StringUtils.isEmpty(html)) {
log.debug("图片替换入参html为空");
return null;
}
// 解析html
Document document = Jsoup.parse(html);
Elements imgList = document.getElementsByTag("img");
if (ObjectUtils.isEmpty(imgList) || imgList.size() == 0) {
log.debug("html中没有图片需要替换");
return html;
}
List<String> srcList = new ArrayList();
for (Element img : imgList) {
// 获取src的值
String src = img.attr("src");
srcList.add(src);
}
log.info("html中img标签src值列表srcList={}", srcList);
// 遍历下载图片
// List imgPathList = downloadImg(srcList);
// 遍历获取图片相对路径
List<String> subImgUrlList = new ArrayList();
for (String imgUrl : srcList) {
// 我这里是用的http图片,所有图片都放在 files 下的,所以从 files/ 后面开始截取
// 获取图片相对路径,并把路径中的 "\" 替换成 "/"
String subImgUrl = imgUrl.substring(imgUrl.indexOf("files") + "files".length() + 1).replaceAll("\\\\", "/");
subImgUrlList.add(subImgUrl);
}
log.info("图片子路径列表subImgUrlList={}", subImgUrlList);
// 替换
for (int i = 0; i < imgList.size(); i++) {
imgList.get(i).attr("src", subImgUrlList.get(i));
}
log.info("图片替换完成后的html={}", document.toString());
return document.toString();
}
/**
* 批量下载图片
*
* @param imgUrlList 图片链接
* @return List 本地存储路径列表
*/
public static List<String> downloadImg(List<String> imgUrlList) {
try {
if (ObjectUtils.isEmpty(imgUrlList) || imgUrlList.size() == 0) {
log.info("图片路径列表入参不能为空");
return null;
}
List<String> imgPathList = new ArrayList();
for (String imgUrl : imgUrlList) {
if (!"".equals(imgUrl)) {
String replaceImgUrl = "";
if (imgUrl.contains("\\")) {
replaceImgUrl = imgUrl.replaceAll("\\\\", "/");
} else {
replaceImgUrl = imgUrl;
}
String fileName = replaceImgUrl.substring(replaceImgUrl.lastIndexOf("/") + 1);
String localImgPath = System.getProperty("user.dir") + "/" + IMG_SAVE_PATH + fileName;
// 下载
URL url = new URL(imgUrl);
URLConnection connection = url.openConnection();
InputStream is = connection.getInputStream();
byte[] bs = new byte[1024];
int len;
File file = new File(localImgPath);
// 图片不存在,下载图片
if (!file.exists()) {
try {
if (file.getParentFile() != null && !file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
FileOutputStream os = new FileOutputStream(file, true);
while ((len = is.read(bs)) != -1) {
os.write(bs, 0, len);
os.flush();
}
os.close();
is.close();
imgPathList.add(localImgPath);
} else {
// 图片存在,直接使用
imgPathList.add(localImgPath);
}
} else {
imgPathList.add(imgUrl);
}
}
return imgPathList;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
package com.cqbay.maserb;
import java.io.File;
/**
* @ClassName: TestMain
* @Description: TODO
* @Author: Jane
* @Date: 2020/7/8 15:22
* @Version: V1.0
**/
public class PdfTest {
// html片段
private static final String content = "\n" +
"犀牛是国bai家稀有动物之一,也是du国家级保护动物。
\n" +
"
\n" +
"犀牛身体庞大bai,四肢粗du壮,体重一般都在三千斤左右。它的皮又厚又硬,足以挡住任何动物的袭击。犀牛鼻子上张着一只或两只坚硬的角,在动物王国里抵抗力和杀伤力都是数一数二的。任何猛兽连人都难以打倒它,它发起怒来连附近的树木植物都难逃厄运,就连狮子、老虎等大型陆地动物在犀牛发怒时都得逃之夭夭。
\n" +
"
\n" +
"犀牛的皮肤虽然厚糙,可皮肤中的细缝却柔嫩,成为了寄生虫、蚊子等吸血昆虫的青睐。可它有一位如影随形的好朋友——犀牛鸟,它以犀牛皮肤空隙里的吸血虫为食。这样既帮助犀牛除出祸害,又让自己饱餐一顿,可真是一举两得啊!
\n" +
"
\n" +
"犀牛拥有高度近视,它的好朋友犀牛鸟却视力良好。在发现情敌的时候,犀牛鸟就会“叽叽喳喳”向犀牛提醒。这时,犀牛就会迅速逃离现场,让敌人枉费心机。
\n" +
"
\n" +
"虽然犀牛以稀有而收到世人的保护,可仍有一些不法之徒向犀牛伸出魔爪,让我们一起来保护犀牛,保护野生动物吧!!!
\n" +
"
";
public static void main(String[] args) {
// 把html片段中的图片url替换成相对url,采用Jsoup解析
String replaceImgTagSrc = PdfUtils.replaceImgTagSrc(content);
//把替换后的html片段根据Thymeleaf模板生成html
String html = PdfUtils.getHtml(PdfUtils.HTML_TEMPLATE_PATH, replaceImgTagSrc);
// 把html转成pdf,这里将生成的pdf文件放在E盘下
PdfUtils.htmlToPdf(html, new File("E:/pdfTest.pdf"));
}
}
参考: