thymeleaf生成html,再转为pdf,加水印,背景色调整

魏翰
2023-12-01

方案有很多:要么依赖于内部组件开发,要么依赖于外部转换工具。

内部组件:

Itext 用于生成PDF文档的一个java类库

flyingSaucer flying saucer是基于itext的,其最大的优势,是对css2.1的支持,页面渲染效果很好。但是也正是不支持css3,所以从最开始的用它,到之后的弃用它,哈哈哈

外部工具:

wkHtmlToPdf,此工具是执行程序,需要安装,并且代码嵌入cmd。

kkFileView 此工具是在线浏览工具,也可以下载。

今天记录一下itext的使用过程。

@PostMapping(value = "/pdfDownload", produces = "application/pdf")
    @ResponseBody
    public Result download(@RequestBody Map<String, Object> map, HttpServletResponse response, HttpServletRequest request) {
        try {
            ProductPdfVo productPdfVo = BeanUtil.fillBeanWithMap(map, new ProductPdfVo(), new CopyOptions());
            log.info("/pdfDownload入参:{}", JSONUtil.toJsonStr(map));
            Map<String, Object> data = BeanUtil.beanToMap(productPdfVo);
            //crm审批
            if ("0".equals(productPdfVo.getFlag())) {
                List<ProductQuotation> productQuotation = productPdfVo.getProductQuotation();
                if (CollUtil.isNotEmpty(productQuotation)) {
                    productQuotation.forEach(x -> {
                        if (StrUtil.isNotBlank(x.getRate()) && !x.getRate().contains("%")) {
                            x.setRateInt(Double.parseDouble(x.getRate()));
                        }
                    });
                }
                List<ServiceQuotation> serviceQuotation = productPdfVo.getServiceQuotation();
                if (CollUtil.isNotEmpty(serviceQuotation)) {
                    serviceQuotation.forEach(x -> {
                        if (StrUtil.isNotBlank(x.getRate()) && !x.getRate().contains("%")) {
                            x.setRateInt(Double.parseDouble(x.getRate()));
                        }
                    });

                }
                List<ConsultQuotation> consultQuotation = productPdfVo.getConsultQuotation();
                if (CollUtil.isNotEmpty(consultQuotation)) {
                    consultQuotation.forEach(x -> {
                        if (StrUtil.isNotBlank(x.getRate()) && !x.getRate().contains("%")) {
                            x.setRateInt(Double.parseDouble(x.getRate()));
                        }
                    });
                }
                List<Maintenance> maintenance = productPdfVo.getMaintenance();
                if (CollUtil.isNotEmpty(maintenance)) {
                    maintenance.forEach(x -> {
                        if (StrUtil.isNotBlank(x.getRate()) && !x.getRate().contains("%")) {
                            x.setRateInt(Double.parseDouble(x.getRate()));
                        }
                    });
                }
                List<ExternalQuotation> externalQuotation = productPdfVo.getExternalQuotation();
                if (CollUtil.isNotEmpty(externalQuotation)) {
                    externalQuotation.stream().forEach(x -> {
                        if (StrUtil.isNotBlank(x.getRate()) && !x.getRate().contains("%")) {
                            x.setRateInt(Double.parseDouble(x.getRate()));
                        }
                    });
                }
                PdfUtil.download(templateEngine, request, "index62", data, response, "ws.pdf",productPdfVo.getFlag());
            } else {
                //无审批
                List<ProductQuotation> productQuotation = productPdfVo.getProductQuotation();
                if (CollUtil.isNotEmpty(productQuotation)) {
                    productQuotation.forEach(x -> {
                        if (StrUtil.isNotBlank(x.getRate())) {
                            x.setRateInt(Double.parseDouble(x.getRate()));
                        }
                        if (StrUtil.isNotBlank(x.getTaxPrice())) {
                            x.setTaxPrice(String.valueOf(Math.round(Double.parseDouble(x.getTaxPrice()))));
                        }
                    });
                }
                List<ServiceQuotation> serviceQuotation = productPdfVo.getServiceQuotation();
                if (CollUtil.isNotEmpty(serviceQuotation)) {
                    serviceQuotation.forEach(x -> {
                        if (StrUtil.isNotBlank(x.getRate())) {
                            x.setRateInt(Double.parseDouble(x.getRate()));
                        }
                        if (StrUtil.isNotBlank(x.getTaxPrice())) {
                            x.setTaxPrice(String.valueOf(Math.round(Double.parseDouble(x.getTaxPrice()))));
                        }
                    });

                }
                List<ConsultQuotation> consultQuotation = productPdfVo.getConsultQuotation();
                if (CollUtil.isNotEmpty(consultQuotation)) {
                    consultQuotation.forEach(x -> {
                        if (StrUtil.isNotBlank(x.getRate())) {
                            x.setRateInt(Double.parseDouble(x.getRate()));
                        }
                        if (StrUtil.isNotBlank(x.getTaxPrice())) {
                            x.setTaxPrice(String.valueOf(Math.round(Double.parseDouble(x.getTaxPrice()))));
                        }
                    });
                }
                List<ExternalQuotation> externalQuotation = productPdfVo.getExternalQuotation();
                if (CollUtil.isNotEmpty(externalQuotation)) {
                    externalQuotation.stream().forEach(x -> {
                        if (StrUtil.isNotBlank(x.getRate())) {
                            x.setRateInt(Double.parseDouble(x.getRate()));
                        }
                        if (StrUtil.isNotBlank(x.getTaxPrice())) {
                            x.setTaxPrice(String.valueOf(Math.round(Double.parseDouble(x.getTaxPrice()))));
                        }
                    });
                }
                PdfUtil.download(templateEngine, request, "index61", data, response, "ws.pdf",productPdfVo.getFlag());
            }
        } catch (Exception e) {
            log.error("pdfDownload异常:{}", e);
            response.setStatus(500);
            return Result.error(e.getMessage());
        }
        return Result.success();
//        html2PdfUtil.html2Pdf("");
//        String html = createHtml("index", "index", data);
//        convertHtmlToImage(html,"ws.png");
    }
package com.cloudwise.cactus.util;

import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.attach.impl.layout.HtmlPageBreak;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.DocumentProperties;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.IBlockElement;
import com.itextpdf.layout.element.IElement;
import com.itextpdf.layout.font.FontProvider;
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Element;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.xml.xmp.XmpWriter;
import lombok.extern.slf4j.Slf4j;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.context.WebContext;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;

//import org.w3c.dom.Document;

/**
 * pdf处理工具类
 *
 * @author gourd.hu
 * @version 1.0
 */
@Slf4j
public class PdfUtil {

    /**
     * 以文件流形式下载到浏览器
     *
     * @param templateEngine 配置
     * @param templateName   模板名称
     * @param listVars       模板参数集
     * @param response       HttpServletResponse
     * @param fileName       下载文件名称
     */
    public static void download(TemplateEngine templateEngine, HttpServletRequest request, String templateName, Map<String, Object> listVars, HttpServletResponse response, String fileName,String flag) {
        //需要添加的水印文字
        String waterMarkName = "信息XXX";
        String waterMarkName1 = "信息yyyy";
        //水印字体透明度
        float opacity = 0.3f;
        //水印字体大小
        int fontsize = 14;
        //水印倾斜角度(0-360)
        int angle = 30;
        //数值越大每页竖向水印越少
        int heightDensity = 20;
        //数值越大每页横向水印越少
        int widthDensity = 5;
        try (ServletOutputStream out = response.getOutputStream()) {
            // 设置编码、文件ContentType类型、文件头、下载文件名
            response.setCharacterEncoding(XmpWriter.UTF8);
//            response.setContentType("application/pdf");
//            response.setHeader("Content-Disposition", "attachment;fileName=" +
//                    new String(fileName.getBytes("gb2312"), "ISO8859-1"));
//            generateAll(templateEngine, request, response, templateName, out, listVars);
            FileOutputStream fileOutputStream = new FileOutputStream(new File("ws.pdf"));
            final Context ctx = new Context();
            ctx.setVariables(listVars);
            String htmlContent = templateEngine.process(templateName, ctx);
//            PdfWriter pdfWriter = new PdfWriter(out);
            PdfWriter pdfWriter = new PdfWriter(fileOutputStream);
            PdfDocument pdfDoc = new PdfDocument(pdfWriter,new DocumentProperties());
            pdfDoc.getDocumentInfo().setTitle(fileName);
//            pdfDoc.setDefaultPageSize(PageSize.A4);// 自定义pageSize
//            Document document = new Document(pdfDoc);
//            PdfFont font = PdfFontFactory.createFont("static/fonts/simhei.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
//            document.setFont(font).setFontSize(10);
            ConverterProperties props = new ConverterProperties();
            FontProvider fp = new FontProvider();
            fp.addFont("static/fonts/simhei.ttf"); // 添加字体文件
            props.setFontProvider(fp);
//            HtmlConverter.convertToPdf(htmlContent, pdfDoc, props);
            List<IElement> elements = HtmlConverter.convertToElements(htmlContent, props);
            Document document = new Document(pdfDoc, PageSize.A4, false);
            document.setMargins(0, 0, 0, 0);
            for (IElement element : elements) {
                // 分页符
                if (element instanceof HtmlPageBreak) {
                    document.add((HtmlPageBreak) element);
                    //普通块级元素
                } else {
                    document.add((IBlockElement) element);
                }
            }
            document.close();
            pdfDoc.close();
            fileOutputStream.flush();
//            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//            byteArrayOutputStream.writeTo(out);
//            byte[] buffer =byteArrayOutputStream.toByteArray();
//            InputStream sbs = new ByteArrayInputStream(buffer);
            if("0".equals(flag)){
                addWaterMark("ws.pdf",out, waterMarkName, opacity, fontsize, angle, heightDensity, widthDensity,false);
            }else{
                addWaterMark("ws.pdf",out, waterMarkName1, opacity, fontsize, angle, heightDensity, widthDensity,false);
            }
            out.flush();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }finally {
            File file=new File("ws.pdf");
            if (file.exists()){
                file.delete();
            }

        }
    }

    /**
     * pdf下载到特定位置
     *
     * @param templateEngine 配置
     * @param templateName   模板名称
     * @param listVars       模板参数集
     * @param filePath       下载文件路径
     */
    public static void save(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, String filePath) {
        try (OutputStream out = new FileOutputStream(filePath);) {
//            generateAll(templateEngine, templateName, out, listVars);
            out.flush();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    /**
     * pdf预览
     *
     * @param templateEngine 配置
     * @param templateName   模板名称
     * @param listVars       模板参数集
     * @param response       HttpServletResponse
     */
    public static void preview(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response) {
        try (ServletOutputStream out = response.getOutputStream()) {
//            generateAll(templateEngine, templateName, out, listVars);
            out.flush();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    /**
     * 按模板和参数生成html字符串,再转换为flying-saucer识别的Document
     *
     * @param templateName 模板名称
     * @param variables    模板参数
     * @return Document
     */
    private static Document generateDoc(TemplateEngine templateEngine, HttpServletRequest request, HttpServletResponse response, String templateName, Map<String, Object> variables) {
        // 声明一个上下文对象,里面放入要存到模板里面的数据
//        final Context context = new Context();
//        context.setVariables(variables);
//        StringWriter stringWriter = new StringWriter();
//        try (BufferedWriter writer = new BufferedWriter(stringWriter)) {
//            templateEngine.process(templateName, context, writer);
//            writer.flush();
//            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
//            return builder.parse(new ByteArrayInputStream(stringWriter.toString().getBytes()));
//        } catch (Exception e) {
            ResponseEnum.TEMPLATE_PARSE_ERROR.assertFail(e);
//        }
        return null;
    }

    /**
     * 核心: 根据Thymeleaf 模板生成pdf文档
     *
     * @param templateEngine 配置
     * @param templateName   模板名称
     * @param out            输出流
     * @param listVars       模板参数
     * @throws Exception 模板无法找到、模板语法错误、IO异常
     */
    private static void generateAll(TemplateEngine templateEngine, HttpServletRequest request, HttpServletResponse response, String templateName, OutputStream out, Map<String, Object> listVars) throws Exception {
        // 断言参数不为空
        //ResponseEnum.TEMPLATE_DATA_NULL.assertNotEmpty(listVars);
        ITextRenderer renderer = new ITextRenderer();
        //设置字符集(宋体),此处必须与模板中的<body style="font-family: SimSun">一致,区分大小写,不能写成汉字"宋体"
        ITextFontResolver fontResolver = renderer.getFontResolver();
        //避免中文为空设置系统字体
        fontResolver.addFont("static/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        // 如果linux下有问题,可使用以下方式解决。
        // 有一个项目是用docker部署的,一直报错找不到simsun.ttf文件,但需要将simsun.ttf上传到/usr/share/fonts
        //fontResolver.addFont(CommonUtil.isLinux() ? "/usr/share/fonts/simsun.ttf" : "static/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

        //根据参数集个数循环调用模板,追加到同一个pdf文档中
        //(注意:此处从1开始,因为第0是创建pdf,从1往后则向pdf中追加内容)
//        for (int i = 0; i < listVars.size(); i++) {
//            Document docAppend = generateDoc(templateEngine, request, response, templateName, listVars.get(i));
            //            renderer.setDocument(docAppend, null);
            WebContext ctx = new WebContext(request, response,request.getServletContext(),request.getLocale(), listVars);
            ctx.setVariables(listVars);
            String content = templateEngine.process(templateName, ctx);
            renderer.setDocumentFromString(content);
            //展现和输出pdf
            renderer.layout();
//            if (i == 0) {
            renderer.createPDF(out);
//            } else {
                //写下一个pdf页面
//                renderer.writeNextDocument();
//            }

//        }
        renderer.finishPDF(); //完成pdf写入
    }
    /**
     * pdf添加水印
     * @param inputFile 需要添加水印的文件
     * @param outputFile 添加完水印的文件存放路径
     * @param waterMarkName 需要添加的水印文字
     * @param opacity 水印字体透明度
     * @param fontsize 水印字体大小
     * @param angle 水印倾斜角度(0-360)
     * @param heightDensity 数值越大每页竖向水印越少
     * @param widthDensity 数值越大每页横向水印越少
     * @param cover 是否覆盖
     * @return
     */
    public static boolean addWaterMark(String inputFile, OutputStream out, String waterMarkName,
                                       float opacity, int fontsize, int angle, int heightDensity, int widthDensity,boolean cover) {
//        if (!cover){
//            File file=new File(outputFile);
//            if (file.exists()){
//                return true;
//            }
//        }
        File file=new File(inputFile);
        if (!file.exists()){
            return false;
        }

        PdfReader reader = null;
        PdfStamper stamper = null;
        try {
            int interval = -5;
            reader = new PdfReader(inputFile);
            stamper = new PdfStamper(reader,out);
            BaseFont base = BaseFont.createFont("static/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            Rectangle pageRect = null;
            PdfGState gs = new PdfGState();
            //这里是透明度设置
            gs.setFillOpacity(opacity);
            //这里是条纹不透明度
            gs.setStrokeOpacity(0.2f);
            int total = reader.getNumberOfPages() + 1;
            System.out.println("Pdf页数:" + reader.getNumberOfPages());
            JLabel label = new JLabel();
            FontMetrics metrics;
            int textH = 0;
            int textW = 0;
            label.setText(waterMarkName);
            metrics = label.getFontMetrics(label.getFont());
            //字符串的高,   只和字体有关
            textH = metrics.getHeight();
            //字符串的宽
            textW = metrics.stringWidth(label.getText());
            PdfContentByte under;
            //循环PDF,每页添加水印
            for (int i = 1; i < total; i++) {
                pageRect = reader.getPageSizeWithRotation(i);
                Rectangle pageRect1 = reader.getPageSize(i);
//                under = stamper.getOverContent(i);  //在内容上方添加水印
                under = stamper.getUnderContent(i);  //在内容下方添加水印
                stamper.setRotateContents(false);
                stamper.setFormFlattening(true);
                PdfShading axial = PdfShading.simpleAxial(stamper.getWriter(),
                        pageRect1.getLeft(pageRect1.getWidth()/10), pageRect1.getBottom(),
                        pageRect1.getRight(pageRect1.getWidth()/10), pageRect1.getBottom(),
                        new BaseColor(245, 245, 245), new BaseColor(245, 245, 245), true, true);
                under.paintShading(axial);
                under.saveState();
                under.setGState(gs);
                under.beginText();
                under.setFontAndSize(base, 10);
                //under.setColorFill(BaseColor.PINK);  //添加文字颜色  不能动态改变 放弃使用
//                under.setFontAndSize(base, fontsize); //这里是水印字体大小
                for (int height = textH; height < pageRect.getHeight() * 2; height = height + textH * heightDensity) {
                    for (int width = textW; width < pageRect.getWidth() * 1.5 + textW; width = width + textW * widthDensity) {
                        // rotation:倾斜角度
                        under.showTextAligned(Element.ALIGN_LEFT, waterMarkName, width - textW, height - textH, angle);
                    }
                }
                //添加水印文字
                under.endText();
            }
            System.out.println("添加水印成功!");
            return true;
        } catch (IOException e) {
            System.out.println("添加水印失败!错误信息为: " + e);
            e.printStackTrace();
            return false;
        } catch (Exception e) {
            System.out.println("添加水印失败!错误信息为: " + e);
            e.printStackTrace();
            return false;
        } finally {
            //关闭流
            if (stamper != null) {
                try {
                    stamper.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                reader.close();
            }
        }
    }
}

工具类中,还写了加水印的,水印需要在pdf生成之后,再加水印,感觉有点费事。直接把outputStream转为InputStream的话,会报错

//            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//            byteArrayOutputStream.writeTo(out);
//            byte[] buffer =byteArrayOutputStream.toByteArray();
//            InputStream sbs = new ByteArrayInputStream(buffer);

 所以,就老老实实的生成pdf再删除吧。背景色是在加水印的代码中体现出来的。

<dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.13</version>
        </dependency>
<dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>html2pdf</artifactId>
            <version>2.1.5</version>
        </dependency>
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

工具类中,我还写了flySaucer的代码,所以,也得加依赖:

<dependency>
            <groupId>org.xhtmlrenderer</groupId>
            <artifactId>flying-saucer-pdf</artifactId>
            <version>9.1.22</version>
        </dependency>
simhei.ttf汉化字体包,百度下载即可。
spring:  
   thymeleaf:
    cache: false
    prefix: classpath:/first/
    mode: HTML
    suffix: .html
    encoding: utf-8
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <style>
        h1{
            color: brown;
        }
        .quotation {
            width: 100%;
        }
        .quotation, .quotation th, .quotation td {
            border: 1px solid brown;
            border-collapse: collapse;
        }
        .quotation th {
            background-color: brown;
            color: white;
        }
    </style>
</head>
<body style="font-family: SimSun">
<h1>Quotation</h1>
<table>
    <tr>
        <th>Customer</th>
    </tr>
    <tr>
        <td  th:text="${#dates.format(quotationDate, 'yyyy-MM-dd')}"></td>
    </tr>
    <tr>
        <td th:text="${#dates.format(offerValidity, 'yyyy-MM-dd')}"></td>
    </tr>
    <tr>
        <td th:text="${customerName}"></td>
    </tr>
</table>
<br />
<table class="quotation">
    <tr>
        <th>序号</th>
        <th>产品名称</th>
        <th>规格及明细</th>
        <th>单价</th>
        <th>单位</th>
        <th>数量</th>
        <th>税率</th>
    </tr>
    <tr th:each="item, iterStat: ${productQuotation}">
        <td th:text="${iterStat.index + 1}"></td>
        <td th:text="${item.productName}"></td>
        <td th:text="${item.details}"></td>
        <td th:text="${item.unitPrice}"></td>
        <td th:text="${item.unit}"></td>
        <td th:text="${item.nums}"></td>
        <td th:text="${item.rate}"></td>
    </tr>
</table>
</body>
</html>

body那加了字体包名,是flySaucer必需的,但不是Itext必需的。

另外要注意,汉化包以及静态文件,需要在pom中配置:

<resources>
            <resource>
                <directory>src/main/resources/</directory>
            </resource>
        </resources>






<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <nonFilteredFileExtensions>
                        <nonFilteredFileExtension>xlsx</nonFilteredFileExtension>
                        <nonFilteredFileExtension>xls</nonFilteredFileExtension>
                        <nonFilteredFileExtension>docx</nonFilteredFileExtension>
                        <nonFilteredFileExtension>html</nonFilteredFileExtension>
                        <nonFilteredFileExtension>css</nonFilteredFileExtension>
                        <nonFilteredFileExtension>png</nonFilteredFileExtension>
<!--                        <nonFilteredFileExtension>ttf</nonFilteredFileExtension>-->
<!--                        <nonFilteredFileExtension>ttc</nonFilteredFileExtension>-->
                    </nonFilteredFileExtensions>
                </configuration>
            </plugin>

build标签的resources标签 与 maven-resources-plugin是异曲同工之妙。

 类似资料: