前言:
本文的目的是将传入的富文本内容(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文件:
七、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;
}