使用LibreOffice将office相关文件(.xls/.xlsx/.doc/.docx)转为pdf

📅 2026/7/4 8:15:40 👁️ 阅读次数 📝 编程学习
使用LibreOffice将office相关文件(.xls/.xlsx/.doc/.docx)转为pdf

LibreOffice

下载地址https://www.libreoffice.org/download/

Java代码示例

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;/*** 使用本机 LibreOffice headless 将 Office 文件转换为 PDF。*/
@Slf4j
@Component
public class LibreOfficePdfConverter {@Value("${file.preview.libre-office.enabled:true}")private boolean enabled;@Value("${file.preview.libre-office.soffice-path:E:\\LibreOffice\\program\\soffice.exe}")private String sofficePath;@Value("${file.preview.libre-office.temp-dir:E:\\libreOfficeTemp}")private String tempDir;@Value("${file.preview.libre-office.timeout-seconds:60}")private long timeoutSeconds;public Optional<byte[]> tryConvertToPdf(byte[] fileBytes, String suffix) {if (!enabled) {return Optional.empty();}if (fileBytes == null || fileBytes.length == 0 || StringUtils.isBlank(suffix)) {return Optional.empty();}Path soffice = Paths.get(sofficePath);if (!Files.isRegularFile(soffice)) {log.warn("LibreOffice soffice不存在,跳过本地转换: {}", sofficePath);return Optional.empty();}Path workDir = null;try {workDir = createWorkDir();String normalizedSuffix = suffix.toLowerCase(Locale.ROOT);Path inputFile = workDir.resolve("source." + normalizedSuffix);Path profileDir = workDir.resolve("profile");Files.createDirectories(profileDir);Files.write(inputFile, fileBytes);Path outputLog = workDir.resolve("soffice.log");List<String> command = Arrays.asList(sofficePath,"--headless","--nologo","--nofirststartwizard","--norestore","-env:UserInstallation=" + profileDir.toUri(),"--convert-to","pdf","--outdir",workDir.toString(),inputFile.toString());Process process = new ProcessBuilder(command).redirectErrorStream(true).redirectOutput(outputLog.toFile()).start();boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);if (!finished) {process.destroyForcibly();log.warn("LibreOffice转PDF超时,suffix={}, timeoutSeconds={}", suffix, timeoutSeconds);return Optional.empty();}if (process.exitValue() != 0) {log.warn("LibreOffice转PDF失败,exitCode={}, output={}", process.exitValue(), readLog(outputLog));return Optional.empty();}Path pdfFile = resolvePdfFile(workDir);if (pdfFile == null || !Files.isRegularFile(pdfFile)) {log.warn("LibreOffice转PDF未生成PDF,output={}", readLog(outputLog));return Optional.empty();}return Optional.of(Files.readAllBytes(pdfFile));} catch (Exception ex) {log.warn("LibreOffice转PDF异常,suffix={}", suffix, ex);return Optional.empty();} finally {deleteQuietly(workDir);}}private Path createWorkDir() throws IOException {if (StringUtils.isBlank(tempDir)) {return Files.createTempDirectory("private-product-preview-");}Path baseDir = Paths.get(tempDir);Files.createDirectories(baseDir);return Files.createTempDirectory(baseDir, "private-product-preview-");}private Path resolvePdfFile(Path workDir) throws IOException {Path expectedFile = workDir.resolve("source.pdf");if (Files.isRegularFile(expectedFile)) {return expectedFile;}try (Stream<Path> stream = Files.list(workDir)) {return stream.filter(path -> StringUtils.endsWithIgnoreCase(path.getFileName().toString(), ".pdf")).findFirst().orElse(null);}}private String readLog(Path outputLog) {try {if (!Files.isRegularFile(outputLog)) {return "";}String text = new String(Files.readAllBytes(outputLog), StandardCharsets.UTF_8);return StringUtils.left(text, 2000);} catch (Exception ignored) {return "";}}private void deleteQuietly(Path workDir) {if (workDir == null || !Files.exists(workDir)) {return;}try (Stream<Path> stream = Files.walk(workDir)) {stream.sorted(Comparator.reverseOrder()).forEach(path -> {try {Files.deleteIfExists(path);} catch (IOException ignored) {// 临时文件清理失败不影响预览结果。
                        }});} catch (IOException ignored) {// 临时目录清理失败不影响预览结果。
        }}
}

注意

依赖软件:
1. LibreOffice
- 需要包含 soffice 命令
- Linux 推荐安装 libreoffice、libreoffice-writer、libreoffice-calc
- 应用配置中的 soffice-path 需要指向 soffice 可执行文件
 
2. 中文字体
- 至少安装 Noto CJK 字体,例如 fonts-noto-cjk
- 如需最大程度还原 Windows/WPS 文档效果,需要补充业务文档中使用的字体,例如宋体、黑体、仿宋、楷体、微软雅黑等
- 字体安装后需要刷新 fontconfig 缓存
 
3. 临时目录
- 应用需要一个可读写的临时目录
- 推荐:/tmp/private-product-preview
 
如果是 K8s 容器部署,建议打进后端镜像里,不要运行时手工装:
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libreoffice \
libreoffice-writer \
libreoffice-calc \
fonts-noto-cjk \
fontconfig \
&& fc-cache -fv \
&& rm -rf /var/lib/apt/lists/*
yaml配置:
file:preview:libre-office:enabled: truesoffice-path: /usr/bin/sofficetemp-dir: /tmp/private-product-previewtimeout-seconds: 60