最近工作中,客户需要生成包含动态内容的word/pdf报告,并且需要在线预览。
刚开始使用后台直接生成word文档,返回文件流给前端,浏览器预览会发生格式错乱问题,特别是文档中的图片有些还不显示。
想到最简单的办法就是后台将docx转换成pdf,前端预览一般就不会出问题。技术栈使用的是java,springboot那一套,所以调研了网上java文件预览相关的技术方案。比较主流的方案有以下几种:
1、Apache POI + Freemarker 或 Thymeleaf
这个算是最底层,最原始的实现方式,可以自定义模板和布局。
示例代码:
// 加载模板
Configuration cfg = new Configuration(Configuration.VERSION_2_3_29);
cfg.setClassForTemplateLoading(MyClass.class, "/templates");
Template template = cfg.getTemplate("template.ftl");
// 数据模型
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("title", "Report Title");
dataModel.put("content", "Report content...");
// 渲染模板到字符串
StringWriter writer = new StringWriter();
template.process(dataModel, writer);
// 创建Word文档
XWPFDocument document = new XWPFDocument();
// 添加内容
XWPFParagraph paragraph = document.createParagraph();
XWPFRun run = paragraph.createRun();
run.setText(writer.toString());
// 保存
FileOutputStream out = new FileOutputStream("report.docx");
document.write(out);
out.close();
缺点:使用起来很麻烦,需要熟悉大量API,自己搞定布局和样式,工作量太大
2、Aspose.Words for Java
这个应该是业界功能最强大的类库,有丰富的api和文档,可以轻松地将Word文档转换为PDF或HTML等格式。
示例代码,将docx文档转为pdf:
private static File docx2Pdf(Long evaluateLogId, File wordFile) throws Exception {
File tempPdf = createTempFile(evaluateLogId + "_final", ".pdf");
try (ByteArrayInputStream docxIn = new ByteArrayInputStream(FileUtil.readBytes(wordFile))) {
String name = "report_" + evaluateLogId + ".pdf";
log.info("开始(docx to pdf)转换: {}", name);
// 设置许可证
License license = new License();
license.setLicense("Aspose.Words.License");
Document doc = new Document(docxIn);
BuiltInDocumentProperties properties = doc.getBuiltInDocumentProperties();
// 设置作者
properties.setAuthor("xxx");
doc.getWatermark().remove();
doc.acceptAllRevisions();
try (FileOutputStream os = new FileOutputStream(tempPdf)) {
doc.save(os, SaveFormat.PDF);
}
log.info("转换完成: {}", name);
}
return tempPdf;
}
上面代码是我使用的网上找的破解版本,需要设置许可证,可以正常转换,没有水印啥的。
需要破解包,可以扫码关注我回复 240813 或 aspose-words
但是,由于系统字体问题,再某些环境下可能有中文乱码问题,我在某一台开发服务器,ububtu系统,按照网上说法。加入中文字体,各种设置最后还是没有解决,时间紧任务重,后面也没有再试了,应该是字体相关配置问题。
缺点:功能要收费
3、Jodconverter + LibreOffice/OpenOffice
jodconverter 是一个 Java 库,用于转换 OpenDocument 和 Microsoft Office 文档格式。它通过与 LibreOffice 或 Apache OpenOffice 连接来完成这些任务。jodconverter 可以在 Java 应用程序中实现自动化文档转换的功能,而无需用户交互。
jodconverter提供了两种模式调用底层Office库:
本地模式(Local Mode)
即在本地直接启动一个 LibreOffice 或 Apache OpenOffice 的实例来进行文档转换,每次转换都需要启动一个新的 LibreOffice 实例,比较耗资源,适合低到中等并发的应用场景。
如果要实现这种模式,需要修改dockerfile,将LibreOffice/OpenOffice集成到自己的应用中,随着容器启动,需要将Office的服务也以无头模式启动起来。
示例如下:
# 使用官方的 Java 运行时作为基础镜像
FROM openjdk:11-jdk-slim
# 设置工作目录
WORKDIR /app
# 将应用的 jar 包复制到容器中
COPY target/my-app.jar my-app.jar
# 下载 LibreOffice
RUN apt-get update && \
apt-get install -y wget && \
wget https://download.documentfoundation.org/libreoffice/stable/7.4/deb/x86_64/LibreOffice_7.4.2_Linux_x86-64_deb.tar.gz && \
tar xzf LibreOffice_7.4.2_Linux_x86-64_deb.tar.gz && \
cd libreoffice* && \
dpkg -i *.deb && \
rm -rf /app/libreoffice* && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# 设置 LibreOffice 无头模式启动命令
RUN echo 'export DISPLAY=:99' >> /etc/environment && \
echo 'export XAUTHORITY=/root/.Xauthority' >> /etc/environment && \
echo 'export HOME=/root' >> /etc/environment && \
mkdir -p /root/.local/share/applications && \
echo "[Desktop Entry]" > /root/.local/share/applications/libreoffice-soffice.desktop && \
echo "Name=LibreOffice (soffice)" >> /root/.local/share/applications/libreoffice-soffice.desktop && \
echo "Exec=/usr/bin/soffice --headless --invisible --nologo --nodefaultfirst --norestore --nocrashreport --nofirststartwizard --accept='socket,host=localhost,port=2002;urp;StarOffice.ServiceManager'" >> /root/.local/share/applications/libreoffice-soffice.desktop && \
echo "Type=Application" >> /root/.local/share/applications/libreoffice-soffice.desktop && \
echo "MimeType=application/msword;application/vnd.openxmlformats-officedocument.wordprocessingml.document;" >> /root/.local/share/applications/libreoffice-soffice.desktop
# 启动 LibreOffice 服务
CMD ["/usr/bin/soffice", "--headless", "--invisible", "--nologo", "--nodefaultfirst", "--norestore", "--nocrashreport", "--nofirststartwizard", "--accept='socket,host=localhost,port=2002;urp;StarOffice.ServiceManager'"] && java -jar my-app.jar
使用代码示例:
public static void main(String[] args) {
8 // 创建 OfficeManager 实例
9 OfficeManager officeManager = new DefaultOfficeManagerBuilder()
10 .officeHome("/path/to/libreoffice") // 指定 LibreOffice 安装路径
11 .build();
12
13 // 启动 OfficeManager
14 officeManager.start();
15
16 try {
17 // 创建 PdfConverter
18 PdfConverter pdfConverter = new PdfConverter(officeManager);
19
20 // 指定源文件和目标文件
21 File source = new File("path/to/source.docx");
22 File target = new File("path/to/target.pdf");
23
24 // 转换文档
25 pdfConverter.convert(source, target);
26
27 System.out.println("Conversion completed.");
28 } catch (Exception e) {
29 e.printStackTrace();
30 } finally {
31 // 停止 OfficeManager
32 officeManager.stop();
33 }
34 }
缺点:jodconverter只是一个基础的中间库,底层依赖于LibreOffice/OpenOffice完成文档转换,配置麻烦
4、kkfileview
上面的方案都太麻烦,在网上找了好久,终于,kkFileView为文件文档在线预览解决方案,该项目使用流行的spring boot搭建,易上手和部署,基本支持主流办公文档的在线预览,如doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,rar,图片,视频,音频等等。
底层原理 还是基于LibreOffice或OpenOffice,只不过kkfileview帮我们封装好了调用逻辑。
详细介绍,及使用文档,请参考官网地址:https://kkfileview.keking.cn/zh-cn/index.html
# 网络环境方便访问docker中央仓库
docker pull keking/kkfileview:4.1.0
# 网络环境不方便访问docker中央仓库
wget https://kkfileview.keking.cn/kkFileView-4.1.0-docker.tar
docker load -i kkFileView-4.1.0-docker.tar
docker run -it -p 8012:8012 keking/kkfileview:4.1.0
项目接入使用
后端返回文件流:
@GetMapping("export-word")
@Parameter(name = "id", description = "编号", required = true, example = "1")
public void exportToWord(@RequestParam("id") Long id, HttpServletResponse response) throws Exception {
wordReportService.exportToWord(id, response);
}
@Override
public void exportToWord(Long id, HttpServletResponse response) {
try {
File doc = generateTmpDoc(id);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Pragma", "No-cache");
ServletUtils.writeAttachment(response, "report.docx", FileUtil.readBytes(doc));
FileUtils.deleteQuietly(doc);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw ServiceExceptionUtil.exception(GlobalErrorCodeConstants.UNKNOWN);
}
}
写一段js脚本测试一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>在线预览文件</title>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/js-base64@3.6.0/base64.min.js"></script>
<script>document.addEventListener("DOMContentLoaded", function () {
var rand = Math.floor(Math.random() * 1000000)
var originUrl = 'http://192.168.8.5:80/admin-api/evaluate/log/report-preview?id=80&rand=' + rand;
// 要预览文件的访问地址
var previewUrl = originUrl + '&fullfilename=report_' + rand + '.docx';
window.open('http://192.168.8.7:8012/onlinePreview?url='+encodeURIComponent(Base64.encode(previewUrl))); });</script>
</head>
<body><h1>test</h1></body>
</html>
预览成功,搞定。