使用 Apache POI 將 PPT/PPTX 轉換為圖片再生成 PDF 時,常遇到中文顯示為方框 (□) 或亂碼。根因:
運行時統一替換所有文本字體為系統確保存在的中文字體 (如「宋體」/「SimSun」),確保渲染引擎能找到字形。
import org.apache.poi.hslf.usermodel.*;
import org.apache.poi.xslf.usermodel.*;
import org.apache.poi.sl.usermodel.*;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.List;
public class PptToPdfConverter {
private static final String TARGET_FONT = "宋體"; // 目標統一字體 (系統必須安裝)
private static final double ZOOM = 2.0; // 渲染縮放倍數 (提高清晰度)
public static void convert(String sourcePath, String destPath) throws Exception {
try (FileInputStream fis = new FileInputStream(sourcePath)) {
String ext = sourcePath.toLowerCase().substring(sourcePath.lastIndexOf('.'));
Document pdfDoc = new Document();
PdfWriter writer = PdfWriter.getInstance(pdfDoc, new FileOutputStream(destPath));
PdfPTable table = new PdfPTable(1);
writer.setInitialLeading(0);
AffineTransform at = new AffineTransform();
at.setToScale(ZOOM, ZOOM);
if (".ppt".equals(ext)) {
// --- PPT (HSLF) 處理 ---
SlideShow ppt = new SlideShow(fis);
Dimension pgSize = ppt.getPageSize();
pdfDoc.setPageSize(new Rectangle(pgSize.width, pgSize.height));
pdfDoc.open();
Slide[] slides = ppt.getSlides();
for (int i = 0; i < slides.length; i++) {
// 關鍵: 統一替換所有文本運行的字體
TextRun[] runs = slides[i].getTextRuns();
for (TextRun run : runs) {
RichTextRun[] richRuns = run.getRichTextRuns();
for (RichTextRun rtr : richRuns) {
rtr.setFontIndex(1); // 強制使用字體索引 1 (通常映射為默認中文字體)
rtr.setFontName(TARGET_FONT);
}
}
// 渲染幻燈片為圖片
BufferedImage img = renderSlide(slides[i], pgSize, at);
addImageToPdf(table, img);
}
ppt.close();
} else if (".pptx".equals(ext)) {
// --- PPTX (XSLF) 處理 ---
XMLSlideShow pptx = new XMLSlideShow(fis);
Dimension pgSize = pptx.getPageSize();
pdfDoc.setPageSize(new Rectangle(pgSize.width, pgSize.height));
pdfDoc.open();
XSLFSlide[] slides = pptx.getSlides();
for (int i = 0; i < slides.length; i++) {
// 關鍵: 遍歷所有形狀,針對文本形狀統一字體
for (XSLFShape shape : slides[i].getShapes()) {
if (shape instanceof XSLFTextShape) {
XSLFTextShape textShape = (XSLFTextShape) shape;
for (XSLFTextParagraph para : textShape.getTextParagraphs()) {
List runs = para.getTextRuns();
for (XSLFTextRun run : runs) {
run.setFontFamily(TARGET_FONT); // 統一字體家族
}
}
}
}
BufferedImage img = renderSlide(slides[i], pgSize, at);
addImageToPdf(table, img);
}
pptx.close();
}
pdfDoc.add(table);
pdfDoc.close();
writer.close();
System.out.println("PPT 轉 PDF 成功: " + destPath);
}
}
// 通用幻燈片渲染方法
private static BufferedImage renderSlide(Slide slide, Dimension pgSize, AffineTransform at) {
BufferedImage img = new BufferedImage(
(int) Math.ceil(pgSize.width * ZOOM),
(int) Math.ceil(pgSize.height * ZOOM),
BufferedImage.TYPE_INT_RGB
);
Graphics2D g = img.createGraphics();
g.setTransform(at);
g.setPaint(Color.WHITE);
g.fill(new java.awt.geom.Rectangle2D.Float(0, 0, pgSize.width, pgSize.height));
slide.draw(g);
g.dispose();
return img;
}
private static BufferedImage renderSlide(XSLFSlide slide, Dimension pgSize, AffineTransform at) {
BufferedImage img = new BufferedImage(
(int) Math.ceil(pgSize.width * ZOOM),
(int) Math.ceil(pgSize.height * ZOOM),
BufferedImage.TYPE_INT_RGB
);
Graphics2D g = img.createGraphics();
g.setTransform(at);
g.setPaint(Color.WHITE);
g.fill(new java.awt.geom.Rectangle2D.Float(0, 0, pgSize.width, pgSize.height));
slide.draw(g);
g.dispose();
return img;
}
private static void addImageToPdf(PdfPTable table, BufferedImage img) throws Exception {
Image pdfImg = Image.getInstance(img, null);
PdfPCell cell = new PdfPCell(pdfImg, true);
cell.setBorder(Rectangle.NO_BORDER);
table.addCell(cell);
}
}
<dependencies>
<!-- POI 核心 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.3.0</version> <!-- 建議用最新版 -->
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.3.0</version>
</dependency>
<!-- iText 5 (PDF 生成) -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
</dependencies>
| 環節 | PPT (HSLF) | PPTX (XSLF) |
|---|---|---|
| 文本容器 | TextRun → RichTextRun | XSLFTextShape → XSLFTextParagraph → XSLFTextRun |
| 字體設置方法 | setFontName() + setFontIndex() | setFontFamily() |
| 多字體問題 | 單字體較少見 | 一頁多字體極常見,必須遍歷所有 Run 統一 |
| 渲染引擎 | Slide.draw(Graphics2D) | XSLFSlide.draw(Graphics2D) |
PDFRenderer 直接輸出 PDF (POI 5.0+ 實驗性支援) 或 LibreOffice headless 轉換 (`soffice --headless --convert-to pdf`),避免 Java2D 渲染瓶頸。PPT/X 轉 PDF 中文方框問題本質是渲染環境字體缺失與多字體統一渲染缺陷。通過運行時遍歷所有文本 Run 統一替換為確保存在的中文字體,配合適當縮放渲染,可解決 95% 以上亂碼場景。生產環境務必驗證目標字體在服務器真實安裝,並考慮 LibreOffice 方案作為兜底。