java 富文本转pdf(支持水印)

发布于:2025-05-01 ⋅ 阅读:(54) ⋅ 点赞:(0)

前言:

本文的目的是将传入的富文本内容(html标签,图片)并且分页导出为pdf。

所用的核心依赖为iText7。

因为itextpdf-core的核心包在maven中央仓库中,阿里云华为云等拉不下来,中央仓库在外网,并且此包在中央仓库中未提供可下载的jar包文件,所以通过iText github上提供的core包的组成jar,实现core包所需方法的调用。

且支持自定义水印

一、jar包导入

在子项目的resources下新建lib目录,下载文件所提供的压缩包,解压jar包文件到lib下,如图所示

二、maven依赖导入

        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>barcodes</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/barcodes-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>commons</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/commons-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>font-asian</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/font-asian-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>forms</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/forms-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>hyph</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/hyph-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>io</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/io-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>kernel</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/kernel-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>layout</artifactId>
            <version>7.2.3</version> <!-- 如果需要布局功能 -->
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/layout-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>pdfa</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/pdfa-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>pdftest</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/pdftest-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>sign</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/sign-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>styled-xml-parser</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/styled-xml-parser-7.2.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>svg</artifactId>
            <version>7.2.3</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/svg-7.2.3.jar</systemPath>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.itextpdf/html2pdf -->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>html2pdf</artifactId>
            <version>4.0.3</version>
        </dependency>

三、打包配置

因为从外部引入jar包,在本地测试没有问题,但是打包后发布,引用不了,所以需要配置打包引用外部jar包。所以可以根据以下链接,博主的另外一个文章提供了相关的方法。

java引用第三方jar包,打包全流程_java引入外部jar包-CSDN博客

四、工具类

import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider;
import com.itextpdf.io.exceptions.IOException;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.source.ByteArrayOutputStream;
import com.itextpdf.kernel.colors.DeviceGray;
import com.itextpdf.kernel.events.Event;
import com.itextpdf.kernel.events.IEventHandler;
import com.itextpdf.kernel.events.PdfDocumentEvent;
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.layout.Canvas;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.font.FontProvider;
import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.layout.properties.VerticalAlignment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

@Component
public class RichTextExporter {

    /**
     * HTML内容转PDF并输出到响应流
     * @param htmlContent HTML内容
     * @param response HttpServletResponse
     * @param request HttpServletRequest(用于获取基础路径)
     * @param fileName 生成的PDF文件名
     * @throws IOException 转换过程异常
     */
    public static void convertToPdf(
            String htmlContent,
            HttpServletResponse response,
            HttpServletRequest request,
            String fileName,
            String watermarkText
    ) throws IOException, java.io.IOException {
        // 配置响应头
        configureResponseHeaders(response, fileName, request);

        try (OutputStream outputStream = response.getOutputStream()) {
            // 创建转换器配置
            ConverterProperties converterProperties = createConverterProperties(request);

            // 判断水印文本是否为空
            if (watermarkText != null && !watermarkText.isEmpty()) {
                // 创建临时的PdfWriter和PdfDocument用于添加水印
                ByteArrayOutputStream tempBaos = new ByteArrayOutputStream();
                PdfWriter tempWriter = new PdfWriter(tempBaos);
                PdfDocument tempPdf = new PdfDocument(tempWriter);
                WatermarkEventHandler watermarkHandler = new WatermarkEventHandler(watermarkText);
                tempPdf.addEventHandler(PdfDocumentEvent.END_PAGE, watermarkHandler);

                // 执行转换到临时PdfDocument
                Document tempDocument = HtmlConverter.convertToDocument(htmlContent, tempPdf, converterProperties);
                tempDocument.close();

                // 将临时的PDF内容写入响应流
                outputStream.write(tempBaos.toByteArray());
            } else {
                // 直接转换到响应流
                HtmlConverter.convertToPdf(htmlContent, outputStream, converterProperties);
            }
        }

    }

    /**
     * 配置HTTP响应头
     */
    private static void configureResponseHeaders(
            HttpServletResponse response,
            String fileName,
            HttpServletRequest request) {
        response.setContentType("application/pdf");
        String attachment = String.format(
                "attachment; filename=\"%s\"",
                encodeFileName(fileName, request)
        );
        response.setHeader("Content-Disposition", attachment);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
    }

    /**
     * 创建转换器配置(处理路径、字体、字符集等)
     */
    private static ConverterProperties createConverterProperties(
            HttpServletRequest request
    ) {
        ConverterProperties properties = new ConverterProperties();

        // 设置基础URI(解析相对路径,如图片/字体地址)
        properties.setBaseUri(getBaseHttpPath(request));

        // 配置字体(支持中文、非嵌入字体、符号)
        FontProvider fontProvider = new DefaultFontProvider(false, false, false);

        try {
            // 使用 ClassPathResource 从 resources 目录下加载字体文件
            ClassPathResource resource = new ClassPathResource("/static/fonts/STSongStd-Light.ttf");
            InputStream inputStream = resource.getInputStream();

            // 使用 ByteArrayOutputStream 读取输入流
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            int nRead;
            byte[] data = new byte[16384];
            while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
                buffer.write(data, 0, nRead);
            }
            buffer.flush();
            byte[] fontBytes = buffer.toByteArray();

            fontProvider.addFont(fontBytes);
        } catch (java.io.IOException e) {
            e.printStackTrace();
        }
        properties.setFontProvider(fontProvider);

        // 设置字符集
        properties.setCharset(StandardCharsets.UTF_8.name());

        return properties;
    }

    /**
     * 获取完整的基础HTTP路径(包含协议、域名、端口、上下文路径)
     */
    private static String getBaseHttpPath(HttpServletRequest request) {
        int port = request.getServerPort();
        String portStr = (port == 80 || port == 443) ? "" : ":" + port;
        return String.format(
                "%s://%s%s%s",
                request.getScheme(),
                request.getServerName(),
                portStr,
                request.getContextPath()
        );
    }

    /**
     * 处理文件名编码(兼容不同浏览器)
     */
    private static String encodeFileName(String fileName, HttpServletRequest request) {
        String userAgent = request.getHeader("User-Agent");
        try {
            if (userAgent.contains("MSIE") || userAgent.contains("Edge") || userAgent.contains("Chrome")) {
                return java.net.URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
            } else {
                return new String(fileName.getBytes(StandardCharsets.UTF_8), "ISO-8859-1");
            }
        } catch (Exception e) {
            return fileName;
        }
    }

    /**
     * 水印事件监听器
     */
    static class WatermarkEventHandler implements IEventHandler {
        private final String watermarkText;
        private final PdfFont font;

        public WatermarkEventHandler(String watermarkText) throws java.io.IOException {
            this.watermarkText = watermarkText;
            // 使用正确的方法重载,第三个参数为 EmbeddingStrategy 枚举(推荐嵌入策略)
            this.font = PdfFontFactory.createFont(
                    "/static/fonts/STSongStd-Light.ttf",  // 引用字体配置文件
                    PdfEncodings.IDENTITY_H,  // 使用标准编码
                    PdfFontFactory.EmbeddingStrategy.FORCE_EMBEDDED  // 优先嵌入字体,确保跨设备显示
            );
        }

        @Override
        public void handleEvent(Event event) {
            PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
            PdfDocument pdf = docEvent.getDocument();
            PdfPage page = docEvent.getPage();
            Rectangle pageSize = page.getPageSize();
            PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamBefore(), page.getResources(), pdf);
            // 修改此处,使用正确的构造函数
            Canvas canvas = new Canvas(pdfCanvas, pageSize);

            // 将角度转换为弧度
            float radAngle = (float) (45 * Math.PI / 180);
            int pageNumber = pdf.getPageNumber(page);

//            // 设置单个水印
//            canvas.setFont(font)
//                    .setFontSize(80) //字体大小
//                    .setFontColor(DeviceGray.GRAY) // 水印颜色
//                    .setOpacity(0.4f) // 透明的, 0-1 越大越不透明
//                    .showTextAligned(
//                            new Paragraph(watermarkText),
//                            pageSize.getWidth() / 2,
//                            pageSize.getHeight() / 2,
//                            pageNumber,
//                            TextAlignment.CENTER,
//                            VerticalAlignment.MIDDLE,
//                            radAngle
//                    );

            // 每页设置多个水印(3行,每行5个)
            drawWatermarks(canvas, pageSize, pageNumber, radAngle, watermarkText,font);

            canvas.close();
        }
    }

    private static void drawWatermarks(Canvas canvas, Rectangle pageSize, int pageNumber, float radAngle, String watermarkText, PdfFont font) {
        int rows = 3; // 水印每页有多少行
        int cols = 5; // 水印每行有多少个

        // 计算水印在水平和垂直方向的间隔
        float xInterval = pageSize.getWidth() / (cols + 1);
        float yInterval = pageSize.getHeight() / (rows + 1);

        for (int i = 1; i <= rows; i++) {
            for (int j = 1; j <= cols; j++) {
                float x = j * xInterval;
                float y = i * yInterval;

                canvas.setFont(font)
                        .setFontSize(10) // 可根据实际情况调整字体大小
                        .setFontColor(DeviceGray.GRAY) // 设置水印字体的颜色
                        .setOpacity(0.4f) // 设置水印字体的透明度,范围0-1,越小越透明
                        .showTextAligned(
                                new Paragraph(watermarkText),
                                x,
                                y,
                                pageNumber,
                                TextAlignment.CENTER,
                                VerticalAlignment.MIDDLE,
                                radAngle
                        );
            }
        }
    }

}

五、controller

    @PostMapping("/export-pdf")
    @Operation(summary = "导出pdf")
    @Parameters({
            @Parameter(name = "htmlContent", description = "富文本内容", required = true),
            @Parameter(name = "watermarkText", description = "水印内容", required = false)
    })
    @Parameter(name = "htmlContent", description = "富文本内容", required = true)
    public void exportPdf(@RequestParam(value = "htmlContent", required = true) String htmlContent,
                          @RequestParam(value = "watermarkText", required = false)  String watermarkText,
                          HttpServletRequest request,
                          HttpServletResponse response) throws IOException {
        RichTextExporter.convertToPdf(htmlContent, response, request, "pdf导出测试", watermarkText);
    }

 六、水印字体的设置

水印字体的配置文件,放到项目的路径下,可自由更改字体配置文件的位置。

修改PdfFontFactory.createFont中的第一个传参即可

ttl字体文件已提供,由于每篇文章仅能引用一个资源,请大家移步博主另外一偏文章获取ttl文件:

Java 富文本转word-CSDN博客

七、yaml配置

因为富文本内容很大,所以如果必须用post请求,并且需要修改上传配置,在yaml文件中加入如下配置即可:

###设置缓冲区大小,方便上传大文件
server:
  tomcat:
    max-swallow-size: 15MB
    max-http-form-post-size: 16777216

八、测试

九、注意

1、标签

经测试,富文本内容最好把ol标签给过滤掉,因为会导致富文本导出为pdf出错,并且如果还需要支持其余特殊的标签,请自行扩展工具类。

2、水印配置

水印参数非必传,支持中文/英文的水印内容。

若需要修改水印配置,则修改drawWatermarks方法中的设置即可。

如下所示:

setFontSize 即设置水印字体大小

setFontColor 即设置水印字体的颜色

setOpacity 即设置水印字体的透明度,范围0-1,越小越透明

private static void drawWatermarks(Canvas canvas, Rectangle pageSize, int pageNumber, float radAngle, String watermarkText, PdfFont font) {
        int rows = 3; // 水印每页有多少行
        int cols = 5; // 水印每行有多少个

        // 计算水印在水平和垂直方向的间隔
        float xInterval = pageSize.getWidth() / (cols + 1);
        float yInterval = pageSize.getHeight() / (rows + 1);

        for (int i = 1; i <= rows; i++) {
            for (int j = 1; j <= cols; j++) {
                float x = j * xInterval;
                float y = i * yInterval;

                canvas.setFont(font)
                        .setFontSize(10) // 可根据实际情况调整字体大小
                        .setFontColor(DeviceGray.GRAY)
                        .setOpacity(0.4f) // 可根据实际情况调整透明度
                        .showTextAligned(
                                new Paragraph(watermarkText),
                                x,
                                y,
                                pageNumber,
                                TextAlignment.CENTER,
                                VerticalAlignment.MIDDLE,
                                radAngle
                        );
            }
        }
    }

十、pdf导出缺少内容问题解决

踩坑了两天终于解决了,太坑了!

问题描述:pdf本地导出内容正常,发布到服务器后缺少内容。

原因:因为iText7的默认字体不支持某些中文字符,所以出现了一些中文字符莫名消失的情况,所以一定要将博主提供ttl字体文件引入设置为导出的默认字体。

即try,catch中的这一部分,强制用ttl字体文件所提供的字体。这一部分在工具类中已修改,放到此处仅做警示。

/**
     * 创建转换器配置(处理路径、字体、字符集等)
     */
    private static ConverterProperties createConverterProperties(
            HttpServletRequest request
    ) {
        ConverterProperties properties = new ConverterProperties();

        // 设置基础URI(解析相对路径,如图片/字体地址)
        properties.setBaseUri(getBaseHttpPath(request));

        // 配置字体(支持中文、非嵌入字体、符号)
        FontProvider fontProvider = new DefaultFontProvider(false, false, false);

        try {
            // 使用 ClassPathResource 从 resources 目录下加载字体文件
            ClassPathResource resource = new ClassPathResource("/static/fonts/STSongStd-Light.ttf");
            InputStream inputStream = resource.getInputStream();

            // 使用 ByteArrayOutputStream 读取输入流
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            int nRead;
            byte[] data = new byte[16384];
            while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
                buffer.write(data, 0, nRead);
            }
            buffer.flush();
            byte[] fontBytes = buffer.toByteArray();

            fontProvider.addFont(fontBytes);
        } catch (java.io.IOException e) {
            e.printStackTrace();
        }
        properties.setFontProvider(fontProvider);

        // 设置字符集
        properties.setCharset(StandardCharsets.UTF_8.name());

        return properties;
    }


网站公告

今日签到

点亮在社区的每一天
去签到