Java使用Thylemeaf + iText实现html(带图片)转pdf文件

基于SpringBoot使用Thymeleaf+iText实现html(带图片)转pdf文件

1.导入依赖

<!-- Thymeleaf 模板引擎 -->
<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.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ognl/ognl 这个依赖不引入会抛异常-->
<dependency>
    <groupId>ognl</groupId>
    <artifactId>ognl</artifactId>
    <version>3.1.12</version>
</dependency>
<!--io常用工具类 -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.7.1</version>
</dependency>

2.创建thylemeaf模板

根据模板生成文件,可以在模板里指定格式,在 resources/templtes目录下创建一个模板

模板图片

htmlTemplate.html文件就是我的模板,font/SIMSUN.TTC文件解决转pdf是解决中文的字体,后面需要引入。

编写模板htmlTemplate.html

<!DOCTYPE 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>
        <!-- 使用 utext 可以识别html标签-->
		<div class="detail-content content_wrap pl30" th:utext="${content}">

		</div>
	</body>
</html>

3.有图片,需要把图片转成 iText 的图片对象,需要转成Base64编码,用到如下类

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) {
    }
}

4.生成PDF的工具类

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<String> 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<String> 本地存储路径列表
     */
    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;
        }
    }
}


5.运行测试

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 = "<p style=\"text-align: justify;\"><img class=\"wscnph\" src=\"http://117.78.37.58:8989/files/20200327\\png\\dcb09254b0d049d28550a9f31d8e88af.png\" /></p>\n" +
            "<p style=\"text-align: left;\">犀牛是国bai家稀有动物之一,也是du国家级保护动物。</p>\n" +
            "<p style=\"text-align: left;\">&nbsp;</p>\n" +
            "<p style=\"text-align: left;\">犀牛身体庞大bai,四肢粗du壮,体重一般都在三千斤左右。它的皮又厚又硬,足以挡住任何动物的袭击。犀牛鼻子上张着一只或两只坚硬的角,在动物王国里抵抗力和杀伤力都是数一数二的。任何猛兽连人都难以打倒它,它发起怒来连附近的树木植物都难逃厄运,就连狮子、老虎等大型陆地动物在犀牛发怒时都得逃之夭夭。</p>\n" +
            "<p style=\"text-align: left;\">&nbsp;</p>\n" +
            "<p style=\"text-align: left;\">犀牛的皮肤虽然厚糙,可皮肤中的细缝却柔嫩,成为了寄生虫、蚊子等吸血昆虫的青睐。可它有一位如影随形的好朋友——犀牛鸟,它以犀牛皮肤空隙里的吸血虫为食。这样既帮助犀牛除出祸害,又让自己饱餐一顿,可真是一举两得啊!</p>\n" +
            "<p style=\"text-align: left;\">&nbsp;</p>\n" +
            "<p style=\"text-align: left;\">犀牛拥有高度近视,它的好朋友犀牛鸟却视力良好。在发现情敌的时候,犀牛鸟就会“叽叽喳喳”向犀牛提醒。这时,犀牛就会迅速逃离现场,让敌人枉费心机。</p>\n" +
            "<p style=\"text-align: left;\">&nbsp;</p>\n" +
            "<p style=\"text-align: left;\">虽然犀牛以稀有而收到世人的保护,可仍有一些不法之徒向犀牛伸出魔爪,让我们一起来保护犀牛,保护野生动物吧!!!</p>\n" +
            "<p style=\"text-align: left;\">&nbsp;</p>";

    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"));
    }

}

6.测试结果

测试结果

参考:<https://www.cnblogs.com/yunfeiyang-88/p/10984740.html

下面是一个使用iText7将HTML换为PDF并添加页数和水印的简单示例: ```java import com.itextpdf.html2pdf.ConverterProperties; import com.itextpdf.html2pdf.HtmlConverter; import com.itextpdf.kernel.colors.ColorConstants; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.kernel.geom.Rectangle; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfPage; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.kernel.pdf.canvas.PdfCanvas; import com.itextpdf.kernel.pdf.xobject.PdfFormXObject; import java.io.File; import java.io.IOException; public class HtmlToPdfConverter { public static void main(String[] args) throws IOException { String html = "<html><body><h1>Hello, World!</h1></body></html>"; String pdfPath = "output.pdf"; PdfWriter writer = new PdfWriter(pdfPath); PdfDocument pdfDoc = new PdfDocument(writer); pdfDoc.setDefaultPageSize(new Rectangle(PageSize.A4.getLeft(), PageSize.A4.getBottom(), PageSize.A4.getRight(), PageSize.A4.getTop())); PdfFont font = PdfFontFactory.createFont("path/to/font.ttf", PdfEncodings.IDENTITY_H, true); PdfCanvas canvas = new PdfCanvas(pdfDoc.addNewPage()); canvas.beginText() .setFontAndSize(font, 12) .moveText(pdfDoc.getDefaultPageSize().getRight() - 72, 36) .showText("Page 1") .endText(); ConverterProperties props = new ConverterProperties(); HtmlConverter.convertToPdf(html, pdfDoc, props); int n = pdfDoc.getNumberOfPages(); for (int i = 2; i <= n; i++) { PdfPage page = pdfDoc.getPage(i); canvas = new PdfCanvas(page); canvas.beginText() .setFontAndSize(font, 12) .moveText(pdfDoc.getDefaultPageSize().getRight() - 72, 36) .showText(String.format("Page %d", i)) .endText(); } PdfFormXObject watermark = new PdfFormXObject(new Rectangle(0, 0, 100, 100)); canvas = new PdfCanvas(watermark, pdfDoc); canvas.setFillColor(ColorConstants.LIGHT_GRAY); canvas.setStrokeColor(ColorConstants.LIGHT_GRAY); canvas.rectangle(0, 0, 100, 100); canvas.fill(); canvas.beginText() .setFontAndSize(font, 24) .moveText(20, 50) .showText("Confidential") .endText(); for (int i = 1; i <= n; i++) { PdfPage page = pdfDoc.getPage(i); canvas = new PdfCanvas(page); canvas.addXObject(watermark, 0, 0); } pdfDoc.close(); } } ``` 在上面的示例中,我们首先创建一个PdfWriter和PdfDocument对象,然后设置默认页面大小为A4纸张大小。接下来,我们添加第一页,并在页面右下角添加“Page 1”文本。然后使用iText7的HtmlConverter将HTML字符串换为PDF文档。使用循环遍历文档中的每个页面,并在每个页面的右下角添加页码。最后,我们创建一个PdfFormXObject对象,添加水印文本“Confidential”,并将其添加到每个页面中。 请注意,上面的示例中的代码片段是未经测试的,仅供参考。您需要根据您自己的需求进行修改和调整。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Freguez · duoyu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值