Java实现Office文档(Word/Excel/PPT)转PDF的完整指南
作者:菩提风
简介:面向 Java 开发者的 Office 文档转 PDF 解决方案包,聚焦 Excel、PPT、Word 批量转换场景,通过 com.aspose 相关组件解除水印与转换次数限制,适合需要在业务系统中集成文档导出、报表归档或课件转存能力的开发与运维人员。包内共 3 个文件,以 zip 压缩包、xml 验证文件与 java 转换工具类为主:压缩包集中放置表格、幻灯片、文字处理三类转换 jar,验证文件用于组件授权校验,工具类提供统一调用入口,便于按项目结构快速接入。压缩包整体约 39.59MB,依赖与示例组织相对集中,可减少自行搜集与适配组件的成本。已有 431 人学习下载,说明该方案在文档转换开发场景中具有一定参考价值。读者可据此了解三类 Office 文档转 PDF 的集成方式、授权文件放置位置及工具类调用思路,并在此基础上完成批量转换、无水印输出与次数限制规避等实际需求。
1. 一份导出后多了行红字的合同:Office 转 PDF 在 Java 里到底卡在哪
上周有同学拿着一份转出来的 PDF 找我,合同正文顶上多了一段红字,另一个 Excel 报表转出来的第一页是张叫 Evaluation Warning 的空表。代码本身没错,是 Aspose 待在评估模式里没出来——许可证没挂上,Word、Excel、PPT 转 PDF 就会被打水印,页数和调用次数也受限制。这类问题在文档中台、报表导出、合同归档的场景里非常典型:功能跑通了,产物不能用。这篇按实际落地的顺序拆一遍,Aspose 三件套怎么引依赖、怎么写出第一段能跑的转换代码、字体和分页怎么调、许可证该在哪一步加载、上线后哪些地方最容易翻车,顺带把 LibreOffice 那条免费路线的水位线摆出来,方便你判断该不该换。
2. 三条路线选型:Aspose 三件套、LibreOffice 和无 Office 依赖的取舍
动手之前先把路线定下来,比写完再返工省事得多。Java 里做 Office 转 PDF,能走的路其实就三条:商业库直接嵌进进程、外部办公套件跑命令行、以及调本机 Office 的 COM 接口。三条路的保真度、部署成本、并发模型完全不一样,选错了后期改造成本很高。我一般会先问三个问题:服务器是不是 Windows、文档复杂度到什么程度、每天的量级是多少。这三个答案基本能锁定路线。
2.1 Aspose 三件套的能力边界
Aspose 在 Java 这边是拆成三个独立 jar 的:Words 管 doc/docx/rtf/odt/html,Cells 管 xls/xlsx/ods/csv,Slides 管 ppt/pptx/odp,各自带一套自己的排版引擎,最终都通过 save 输出 PDF。它的最大优势是纯 Java、进程内调用、不依赖本机装 Office,扔进 Linux 容器就能跑,这对现在普遍容器化部署的环境很关键。API 风格也统一,三个库都有 License 类和 save 方法,学会一个基本能推另外两个。
边界也要说清楚。第一,它是商业授权产品,评估模式下会插入水印并限制处理规模,正式使用需要对应的许可证文件;网上流传的替换类文件、改字节码那类做法我不建议碰,产物不稳定、升级就崩,而且风险不在技术层面。第二,jar 体积不小,三个包叠起来会让镜像变大,冷启动首次加载有明显延迟。第三,内存峰值高,一个几十页带图的 docx 转换时占用几百 MB 很常见,容器内存 limit 设小了会直接被 OOMKill,这个坑后面还会提。
许可证到位之后是没有调用次数限制的,转换次数取决于你的机器性能而不是软件本身,这点和按次计费的云 API 不一样,量大时单次成本会摊得很低。至于水印到底从哪来、许可证该在哪一步加载,第 3 章会单独拆。
2.2 LibreOffice headless 的真实水位线
不想买授权,第一条替代路线是 LibreOffice 的命令行转换。它本质上是一个完整的办公套件以无界面模式跑起来,加载文档、渲染、导出 PDF,格式覆盖非常广,Word 的复杂表格、PPT 的母版基本都能还原个八九不离十。
# 单文件转换,writer_pdf_Export 是 Word 的导出过滤器 soffice --headless --norestore --invisible \ --convert-to pdf:writer_pdf_Export \ --outdir /data/out /data/in/contract.docx
命令本身简单,但直接拿去上生产会踩两个坑。一是用户配置目录,headless 模式下如果 HOME 不可写或者多个进程共用同一个 profile,会偶发启动失败或者转换卡住,稳妥做法是每次给一个独立的 profile 目录:
# 每个进程用独立 profile,避免并发时互相锁死 soffice --headless --norestore \ -env:UserInstallation=file:///tmp/lo_profile_$$ \ --convert-to pdf --outdir /data/out /data/in/report.xlsx
二是性能模型完全不同。它不是进程内调用,每次转换都要拉起一个重量级进程,冷启动一两秒起步,并发一上来 CPU 和内存直接打满,必须做成进程池 + 队列,还要有超时杀进程的兜底,因为 LibreOffice 偶发会卡死在某份文档上不返回。保真度上,它对 Word 的分页符、文本框定位、特殊字体的处理和 Office 有可见偏差,合同、标书这类排版敏感的文档要提前做样例回归。
2.3 documents4j 与 COM 路线
第三条是调本机装的 Office,documents4j 是比较常见的封装。它的原理是把文档丢给 Word/Excel/PowerPoint 进程去另存为 PDF,所以保真度是三条路里最高的——毕竟就是 Office 自己在渲染。
// 依赖本机安装 Office,仅适合 Windows 服务器
IConverter converter = LocalConverter.builder().build();
converter.convert(new File("D:/in/report.docx"))
.as(DocumentType.DOCX)
.to(new File("D:/out/report.pdf"))
.as(DocumentType.PDF)
.execute();代价也很直接:服务器必须是 Windows,必须装正版 Office,进程不稳定(Office 弹个对话框就挂住),并发能力差,通常要用单独的转换机 + 消息队列隔离。我见过用这种方案的系统,最后都演变成一台专门的 Windows 机器在扛全部转换请求,扩容很痛苦。
2.4 一张对比表背后的判断标准
| 维度 | Aspose 三件套 | LibreOffice headless | documents4j / COM |
|---|---|---|---|
| 运行环境 | 纯 Java,Linux 容器可跑 | 需装 LibreOffice,Linux 可跑 | 必须 Windows + Office |
| 保真度 | 高,复杂排版基本还原 | 中,分页和文本框有偏差 | 最高,Office 原生渲染 |
| 并发模型 | 进程内多线程,需控内存 | 多进程池,需超时兜底 | 单机串行,靠队列削峰 |
| 单次耗时 | 百毫秒到数秒 | 秒级,冷启动更久 | 秒级,受 Office 启动影响 |
| 成本 | 商业授权 | 免费 | Office 授权 + Windows 机器 |
| 升级维护 | jar 升级,需回归 | 系统包升级,影响面大 | Office 版本绑定 |
判断标准其实就一句话:文档要对外交付、排版不能出错、量还不小,那就走商业库;内部预览、对排版容忍度高,LibreOffice 够用;存量系统全在 Windows 且已经有 Office 授权,documents4j 是顺水推舟。三条路没有绝对优劣,只有部署约束。
3. 把依赖和第一段代码跑通:Word、Excel、PPT 三套 API 的最小差异
选定路线之后就是落地。这一章把依赖、三个格式的转换代码、以及许可证加载这条链路完整走一遍。三个库的 API 长得很像但细节不同,混着写最容易出的问题是包名冲突和选项类张冠李戴,下面逐个拆。
3.1 Maven 依赖与版本对齐
Aspose 的包不在中央仓库,需要单独配仓库地址。三个包建议版本号保持一致,混版本出现 NoSuchMethodError 的概率不低。
<repositories>
<repository>
<id>aspose-repo</id>
<url>https://releases.aspose.com/java/repo/</url>
</repository>
</repositories>
<properties>
<!-- 三件套统一版本,具体数字以你手上的授权包为准 -->
<aspose.version>你的版本号</aspose.version>
</properties>
<dependencies>
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-words</artifactId>
<version>${aspose.version}</version>
</dependency>
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-cells</artifactId>
<version>${aspose.version}</version>
</dependency>
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-slides</artifactId>
<version>${aspose.version}</version>
</dependency>
</dependencies>这里有个容易被忽略的点:如果公司有内网 Nexus,需要把上面这个仓库代理进去,否则 CI 构建会因为拉不到包直接失败。另外三个 jar 加起来体积不小,打 Docker 镜像时尽量放在独立 layer,避免每次改业务代码都重传几十 MB。
3.2 Word 转 PDF 的最小可运行示例
Words 的入口是 Document ,加载后调 save 指定 SaveFormat.PDF 。真正上生产不能只写这两行,导出选项要配,域和分页要先刷新,否则页眉里的章节号、目录页码会是旧值。
import com.aspose.words.*;
public class WordToPdf {
public static void convert(String src, String dest) throws Exception {
// 加载文档,LoadOptions 里可以控制是否加载修订、是否保留嵌入字体
Document doc = new Document(src);
// 先更新域(目录、页码、交叉引用),再刷新分页布局
// 不调这两步,导出的 PDF 里目录页码大概率还是上一次保存时的值
doc.updateFields();
doc.updatePageLayout();
PdfSaveOptions opts = new PdfSaveOptions();
// PDF/A 适合归档场景,普通业务用默认即可
opts.setCompliance(PdfCompliance.PDF_17);
// 开启输出优化,能明显减小含大量图形元素的文档体积
opts.setOptimizeOutput(true);
// 导出书签,长文档很实用
opts.setExportBookmarksInHeadingsOutline(true);
doc.save(dest, opts);
}
}
setCompliance 选什么取决于用途:对外归档、要过合规检查的选 PDF/A 系列,普通流转用 PDF 1.7 甚至默认就行,PDF/A 会禁用部分特性,字体嵌入要求也更严。 updatePageLayout() 是耗时操作,长文档上可能占整体时间的三成,但跳过它分页会错,别省。
3.3 Excel 与 PPT 的转换差异点
Cells 和 Slides 的写法结构一样,但选项类完全不同,导出效果差异也大。Excel 最典型的问题是宽表被切断、一页纸上挤了半张表。
import com.aspose.cells.*;
public class ExcelToPdf {
public static void convert(String src, String dest) throws Exception {
Workbook wb = new Workbook(src);
// 大文件建议开内存优化,代价是部分特性不可用
wb.getSettings().setMemorySetting(MemorySetting.MEMORY_PREFERENCE);
// 指定打印区域和纸张,避免宽表被横向截断
Worksheet ws = wb.getWorksheets().get(0);
PageSetup ps = ws.getPageSetup();
ps.setOrientation(PageOrientationType.LANDSCAPE);
ps.setPaperSize(PaperSizeType.PAPER_A4);
ps.setFitToPagesWide(1); // 宽度压到一页
ps.setFitToPagesTall(0); // 高度不限,允许多页
PdfSaveOptions opts = new PdfSaveOptions();
// 每个 sheet 强制从新页开始,不设会出现两个 sheet 挤一页
opts.setOnePagePerSheet(true);
opts.setAllColumnsInOnePagePerSheet(false);
wb.save(dest, opts);
}
}
setFitToPagesWide(1) 和 setFitToPagesTall(0) 这两个参数是 Excel 导出的关键组合,含义分别是「宽度缩放到一页」和「高度自动」。只设宽度不设高度,超长表格会被强行压成一页导致字小到看不清;只设高度不设宽度,宽表会被横向切断。 setMemorySetting 在小文件上反而略慢,别盲目开。
Slides 相对简单, Presentation 加载后保存即可,需要注意的是图片质量和文字压缩:
import com.aspose.slides.*;
public class PptToPdf {
public static void convert(String src, String dest) throws Exception {
Presentation pres = new Presentation(src);
try {
PdfOptions opts = new PdfOptions();
// 图片质量,0-100,导出归档建议 90 以上
opts.setJpegQuality((byte) 90);
// 文字压缩级别,设为不压缩可保证可检索
opts.setTextCompression(PdfTextCompression.None);
pres.save(dest, SaveFormat.Pdf, opts);
} finally {
// Presentation 持有文件句柄,必须释放
pres.dispose();
}
}
}
dispose() 这行别漏。Slides 对象持有资源,在批量转换场景下不释放,跑几百个文件后会出现句柄耗尽或者内存缓慢上涨,排查起来很费劲。
3.4 把水印这件事说清楚:许可证该在哪加载
回到开头那份带红字的合同。Aspose 三件套在未加载有效许可证时进入评估模式,具体表现是:Words 会在文档开头插入评估声明段落,Cells 会额外生成一个 Evaluation Warning 工作表(转 PDF 后变成第一页),Slides 会在每页加评估水印。这些不是 bug,是设计如此。
正确的做法是在任何文档对象创建之前加载许可证文件,并且三个库各自加载各自的:
import java.io.InputStream;
public final class AsposeLicense {
private static volatile boolean ready = false;
public static synchronized void init() {
if (ready) return;
try {
// 注意:三个库的 License 类同名,必须写全限定名,否则编译冲突
try (InputStream w = AsposeLicense.class.getResourceAsStream("/lic/aspose.words.lic")) {
new com.aspose.words.License().setLicense(w);
}
try (InputStream c = AsposeLicense.class.getResourceAsStream("/lic/aspose.cells.lic")) {
new com.aspose.cells.License().setLicense(c);
}
try (InputStream s = AsposeLicense.class.getResourceAsStream("/lic/aspose.slides.lic")) {
new com.aspose.slides.License().setLicense(s);
}
ready = true;
} catch (Exception e) {
// 这里必须抛出或告警,吞掉异常等于静默回到评估模式
throw new IllegalStateException("Aspose 许可证加载失败", e);
}
}
}
三个要点。第一, com.aspose.words.License 和 com.aspose.cells.License 同名,同一文件里都 import 通配符会直接编译不过,必须全限定名。第二, getResourceAsStream 的路径带前导斜杠,指向 classpath 根,打成 jar 后也有效;如果读出来是 null,说明资源没被打进包,检查 Maven 的 resources 配置。第三,也是最容易出事的: setLicense 失败会抛异常,很多人 catch 之后只打了一行日志就继续跑,结果服务看起来正常,产物全是带水印的。这条链路要么启动时快速失败,要么至少打 ERROR 并接告警。
许可证文件本身要和 jar 版本匹配、在有效期内,版本对不上会抛类似「许可证对此产品无效」的异常。想让产物真正无水印、无次数限制,正路只有一条:拿到对应版本的正式许可证并正确加载。至于许可证内容怎么放进容器,我一般用环境变量注入或者挂载 Secret,不塞进代码仓库。
提示:转换失败和水印问题是两条链路。看到产物带水印先查许可证加载日志,别去翻转换代码。
4. 排版保真:字体、分页、图片这三处决定了客户会不会打回
代码跑通只是及格线,交付物能不能过业务方的眼,取决于三个细节:中文字体有没有、分页是不是符合预期、图片清不清楚。这三点在本地开发机上往往看不出来,一进容器就全暴露,下面按踩坑频率排。
4.1 中文字体:容器里没有宋体怎么办
这是最经典的一类问题。开发机是 Windows,宋体、黑体、微软雅黑都在,转出来一切正常;镜像基于精简版 Linux,一个中文字体都没有,转出来的 PDF 里中文要么变成方框,要么整段被替换成别的字体导致串行、断行位置全变。
解决思路是把字体文件随镜像一起带进去,并显式告诉 Aspose 去哪里找。Words 用 FontSettings :
import com.aspose.words.FontSettings;
import com.aspose.words.FontSourceBase;
import com.aspose.words.FolderFontSource;
import com.aspose.words.SystemFontSource;
import java.util.ArrayList;
import java.util.List;
public class FontConfig {
public static void init(String fontDir) {
List<FontSourceBase> sources = new ArrayList<>();
// 优先用镜像里自带的字体目录,保证多环境一致
sources.add(new FolderFontSource(fontDir, true));
// 再兜底系统字体,避免个别字体缺失导致整体失败
sources.add(new SystemFontSource());
FontSettings.getDefaultInstance().setFontsSources(sources);
// 字体替换告警:缺字体时能第一时间看到是哪个字体没找到
FontSettings.getDefaultInstance().setFontSubstitutionSettings(
buildSubstitution());
}
private static com.aspose.words.FontSubstitutionSettings buildSubstitution() {
com.aspose.words.FontSubstitutionSettings s =
new com.aspose.words.FontSubstitutionSettings();
// 找不到字体时记录告警而不是静默替换
s.getDefaultFontSubstitution().setDefaultFontName("SimSun");
return s;
}
}
Cells 的字体配置走的是另一套 API, CellsHelper.setFontDirs ,别拿 Words 的方法往 Cells 上套:
// Cells 独立配置字体目录,与 Words 的 FontSettings 互不影响
com.aspose.cells.CellsHelper.setFontDirs(new String[]{"/opt/fonts"});
字体文件从哪来是个现实问题。商用字体(比如某些中文字体)有授权限制,不能随便打包分发,我一般只用开源可商用的字体集,或者在部署时把客户已授权的字体挂载进去。镜像构建时可以加一步校验,确认关键字体确实存在:
# 构建期检查,缺字体直接让 CI 失败,别等到线上才发现 fc-list :lang=zh | grep -i -E "simsun|noto.*cjk" || \ (echo "缺少中文字体,构建终止" && exit 1)
4.2 Excel 宽表分页与打印区域
Excel 转 PDF 的分页逻辑和 Word 完全不是一回事。Word 是流式排版,按纸张高度自动断页;Excel 是按工作表 + 打印区域来的,不设打印区域时它会把有内容的整个范围当成打印区,几十列的表横着铺出去,PDF 里就是一堆被切碎的列。
实操上我一般按三步走。先确定业务方要的输出形态:是要一张横向 A4 把所有列塞进去,还是允许分页但要保证表头重复。前者用第 3 章的 setFitToPagesWide(1) ;后者要配 setPrintTitleRows 让每页都带表头:
PageSetup ps = ws.getPageSetup();
// 前两行作为表头,分页时每页重复
ps.setPrintTitleRows("$1:$2");
// 明确打印区域,避免空白列被算进去
ps.setPrintArea("A1:N80");
// 页脚加页码,方便业务方核对
ps.setFooter(0, "第 &P 页 / 共 &N 页");
这三行解决的是最常见的三个投诉:「第二页没有表头」「右边多出来一堆空白列」「打印出来不知道第几页」。另外合并单元格跨页的问题基本无解,能做的只有提前调整行高或者拆表,遇到这种文档建议在业务侧就规避。
4.3 图片压缩与清晰度
PDF 体积和清晰度是一对矛盾。默认参数下,含大量高清图的 PPT 转出来的 PDF 可能几十 MB,走邮件或者移动端下载都很痛苦;把 setJpegQuality 压到 60 以下,图上的小字就糊了。
我的处理方式是按用途分两档:流转预览用质量 70 左右,归档用 90 以上并关掉额外压缩,两档各出一个文件,前端按场景取。Words 这边对应的是 PdfSaveOptions.setJpegQuality 和 setImageCompression ,PPT 用 PdfOptions ,参数名不同但逻辑一致。压之前最好先统计一下原文档里的图片总量,超过阈值就强制走预览档,别让用户等一个几十秒的转换。
5. 排查五个高频翻车:水印、字体、内存、并发和进程卡死
上线之后的问题大概集中在五个地方,这里按「现象 → 原因 → 解决」整理成可以直接对照的清单。这些都是实际遇到过、并且复现过的,不是理论推演。
5.1 转换结果不对的三类问题
水印突然出现,之前一直正常。 现象是某天开始所有产物都带上评估声明,代码没动过。原因通常是许可证文件过期,或者新版本 jar 部署后许可证与版本不匹配,也可能是打包时 resources 目录漏配导致 getResourceAsStream 返回 null,而初始化代码把异常吞了。解决办法是把许可证加载改成启动时快速失败:加载失败直接不启动,同时在健康检查里加一个「转换一个内置样例文档并检查产物文本」的探针。这样问题会在发布阶段暴露,不会等到业务方投诉。
中文变成方框或者乱码。 现象是本地正常、容器里全是方块,或者部分生僻字丢失。原因是运行环境缺字体,且没有配置字体目录。解决是把字体文件打进镜像,Words 用 FontSettings 指定 FolderFontSource ,Cells 用 CellsHelper.setFontDirs ,并在构建期加 fc-list 校验。这里还有个隐藏变体:字体文件在,但文件名和系统注册名不一致,Aspose 按内部名称匹配,找不到仍然会替换,建议用官方字体而不是网上随手下的改名文件。
分页位置和 Word 里看到的不一样。 现象是导出后某一页的内容跑到下一页,或者表格跨页断在奇怪的位置。原因是转换引擎和 Office 的排版算法不完全一致,行高计算、字体度量、段落间距都可能差一点点,累积到页尾就顶过去了。解决思路不是调参数,而是减少依赖自动分页:关键位置手动插分页符,表格设置「允许跨页断行」的策略,长文档做样例回归并接受小范围差异。要求像素级一致的项目,得在验收标准上提前对齐。
5.2 资源与稳定性相关的两类问题
服务跑一段时间后 OOM。 现象是刚发布时正常,跑几小时后容器被 OOMKill,重启后恢复,循环往复。原因通常是 Slides 的 Presentation 没调 dispose() ,或者 Cells 处理大文件时没开内存优化,对象一直挂在内存里等 GC。解决办法是给所有流和文档对象加 try-with-resources 或者 finally 释放,批量任务里每处理完一个文件显式置空引用,监控上把堆内存和转换耗时一起看,两者同时抬升基本就是没释放。容器 limit 也别卡太死,一个 512MB 的容器转几十页带图的文档是不够的。
并发一上来就卡住不返回。 现象是单线程测试完全正常,压测到几十并发后请求堆积,日志里看不到异常,就是没响应。原因是转换本身是 CPU 密集 + 高内存操作,线程数超过核数后互相争抢,加上某些文档会触发超长路径(比如嵌套很深的表格),单次耗时从 1 秒变成 60 秒。解决办法是三件事:线程池大小按 CPU 核数控制而不是拍脑袋定 200;给每次转换加超时,超时后放弃结果并记录源文件用于复现;请求侧做排队和限流,把并发压力挡在转换层之前。这里有个坑要提前知道:转换过程不响应线程中断, Future.cancel 之后线程可能还在跑,所以真正的隔离要么靠独立进程,要么靠线程池的容量控制,不能指望取消。
6. 并发与结果校验:把转换服务从能跑到敢上线
转换服务的稳定性,一半靠线程模型,一半靠对结果的自动校验。我现在的做法是把「转换」和「验证」拆成两步:转换完不直接返回,先跑一个轻量校验,确认产物没有水印、页数不为零、关键文本能提取出来,通过了才进结果库。这一步能把绝大多数问题挡在交付之前。
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
public class PdfValidator {
public static void validate(String pdfPath, String... mustContain) throws Exception {
try (PDDocument pdf = PDDocument.load(new java.io.File(pdfPath))) {
if (pdf.getNumberOfPages() == 0) {
throw new IllegalStateException("产物页数为 0");
}
String text = new PDFTextStripper().getText(pdf);
// 评估水印的典型特征串,命中即判定转换不可用
if (text.contains("Evaluation Only") || text.contains("Created with an evaluation")) {
throw new IllegalStateException("检测到评估水印,检查许可证加载链路");
}
// 关键字段校验,比如合同编号、报表标题必须出现
for (String kw : mustContain) {
if (!text.contains(kw)) {
throw new IllegalStateException("产物缺少关键内容: " + kw);
}
}
}
}
}
这个校验器的价值在于把「肉眼发现问题」变成「系统自动拦截」。水印特征串还要根据实际产物调整,不同库插入的文案不一样,第一次上线时把带水印的样例跑一遍,把实际文案补进判断条件。 mustContain 那部分是从业务侧借来的:合同必须有编号,报表必须有标题行,这类校验比页数检查更能发现内容截断。
线程模型上,我一般用一个固定大小的转换线程池,容量取 CPU 核数的一半到一倍之间,队列有上限,满了直接拒绝而不是无限堆积。每个任务外面包一层超时,超时的源文件单独落盘到一个「疑难样本」目录,攒够一批统一分析,比一个个现场排查效率高得多。转换任务是 CPU 密集型的,和业务线程池混用会互相拖累,物理隔离是比较稳妥的选择。
最后说个血泪经验。早期我觉得转换就是调个 save ,能出文件就算成功,结果交付给业务方的第一批 PDF 里混着带水印的、字体缺失的、还有一页是空白的。从那以后我每次接这类需求,都强制走一遍「内置样例 + 自动校验 + 人工抽检」三关,样例覆盖中文、宽表、多图、长文档四类,校验脚本进 CI,抽检每周固定做一次。这套流程看着笨,但能把大部分翻车挡在交付之前。
以上就是Java实现Office文档(Word/Excel/PPT)转PDF的完整指南的详细内容,更多关于Java Office文档转PDF的资料请关注脚本之家其它相关文章!
