SpringBoot中PDF处理完全指南
文章目录
- 1. PDF基础知识
- 1.1 什么是PDF
- 1.2 PDF文件结构
- 2. SpringBoot中的PDF处理库
- 2.1 iText
- 2.2 Apache PDFBox
- 2.3 OpenPDF
- 2.4 JasperReports
- 2.5 选择哪个库?
- 3. 生成PDF文件
- 3.1 使用iText生成PDF
- 基本PDF文档生成
- 添加表格
- 添加图片
- 3.2 使用Apache PDFBox生成PDF
- 基本文档生成
- 添加表格(PDFBox中较为复杂)
- 3.3 使用OpenPDF生成PDF
- 3.4 在SpringBoot控制器中生成并下载PDF
- 4. 读取与解析PDF
- 4.1 使用PDFBox读取PDF文本
- 4.2 使用iText解析PDF
- 4.3 从PDF中提取表格数据
- 5. 修改现有PDF文件
- 5.1 添加水印和页码
- 使用iText添加水印
- 使用PDFBox添加页码
- 5.2 合并多个PDF文件
- 使用PDFBox合并PDF
- 使用iText合并PDF
- 5.3 分割PDF文件
- 5.4 加密与解密PDF
- 5.5 删除和重新排序页面
- 5.6 填充PDF表单
- 6. Web应用中的PDF处理
- 6.1 PDF下载功能实现
- 6.2 在线PDF预览实现
- 6.3 PDF文件上传和处理
- 6.4 批量PDF处理
- 7. PDF安全性与高级功能
- 7.1 PDF文档加密与权限控制
- 7.2 添加水印
- 7.3 数字签名
- 7.4 PDF表单与交互功能
- 8. PDF处理最佳实践
- 8.1 性能优化
- 8.1.1 内存管理
- 8.1.2 并行处理
- 8.2 异常处理与日志
- 8.3 安全性建议
- 8.3.1 文件上传安全性
- 8.3.2 敏感信息保护
- 8.4 测试PDF处理功能
- 8.5 部署最佳实践
- 8.6 错误处理与恢复机制
- 9. 常见问题与解决方案
- 9.1 乱码与字体问题
- 问题:PDF中中文显示为方框或乱码
- 9.2 图像处理问题
- 问题:图片在PDF中模糊或变形
- 问题:PDF文件大小过大
- 9.3 表单与交互性问题
- 问题:填充PDF表单后字段值不显示
- 问题:无法在PDF中添加交互式元素
- 9.4 性能与内存问题
- 问题:处理大型PDF文件时内存溢出(OutOfMemoryError)
- 问题:PDF处理速度慢
- 9.5 安全问题
- 问题:如何防止PDF注入攻击
- 问题:如何安全地处理上传的PDF文件
- 9.6 布局与分页问题
- 问题:内容跨页不正确或分页不合理
- 9.7 PDF转换问题
- 问题:如何将HTML转换为PDF
- 问题:如何将PDF转换为图片
- 9.8 Spring Boot 集成问题
- 问题:如何在Spring Boot中优雅地处理PDF生成失败
1. PDF基础知识
1.1 什么是PDF
PDF(Portable Document Format,便携式文档格式)是由Adobe公司开发的一种电子文件格式,旨在独立于应用软件、硬件和操作系统,呈现文档的固定布局。PDF具有以下特点:
- 跨平台兼容性:可以在任何操作系统上查看,保持相同的外观
- 文档完整性:包含文本、图像、表格、字体等所有文档元素
- 紧凑性:支持多种压缩技术
- 安全性:可以设置密码和权限
- 交互性:支持超链接、表单、多媒体等交互元素
1.2 PDF文件结构
PDF文件包含四个主要部分:
- 头部(Header):标识PDF版本
- 主体(Body):包含文档内容(文本、图像等)
- 交叉引用表(Cross-reference Table):提供文档对象位置的索引
- 尾部(Trailer):包含指向交叉引用表的指针和其他对象的引用
了解这些基础概念对于理解PDF操作库的工作原理很有帮助。
2. SpringBoot中的PDF处理库
在SpringBoot应用中处理PDF文件,有几个流行的Java库可供选择:
2.1 iText
iText是一个功能强大的PDF处理库,适用于生成、修改和分析PDF文档。
Maven依赖:
<!-- iText核心库 -->
<dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.13.3</version>
</dependency><!-- iText 7 (更新版本) -->
<dependency><groupId>com.itextpdf</groupId><artifactId>itext7-core</artifactId><version>7.2.5</version><type>pom</type>
</dependency>
注意:iText有开源版本(AGPL许可)和商业版本。在商业项目中使用前,请确认许可证要求。
2.2 Apache PDFBox
Apache PDFBox是Apache软件基金会的开源PDF库,功能全面,许可证更加开放(Apache License 2.0)。
Maven依赖:
<dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.27</version>
</dependency>
2.3 OpenPDF
OpenPDF是iText 4.2.0的开源继承者,提供了更灵活的许可证(LGPL/MPL)。
Maven依赖:
<dependency><groupId>com.github.librepdf</groupId><artifactId>openpdf</artifactId><version>1.3.30</version>
</dependency>
2.4 JasperReports
JasperReports是一个用于生成PDF报表的高级库,特别适合复杂报表的生成。
Maven依赖:
<dependency><groupId>net.sf.jasperreports</groupId><artifactId>jasperreports</artifactId><version>6.20.0</version>
</dependency>
2.5 选择哪个库?
- iText: 功能最全面,适合需要高度自定义的场景,但许可限制需要注意
- Apache PDFBox: 开源友好,适合基本PDF操作,API相对低级
- OpenPDF: 适合需要iText功能但关注许可问题的项目
- JasperReports: 最适合复杂报表生成,学习曲线较陡
3. 生成PDF文件
3.1 使用iText生成PDF
基本PDF文档生成
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.pdf.PdfWriter;import java.io.FileOutputStream;
import java.io.IOException;@Service
public class PdfGenerationService {public void generateSimplePdf(String outputPath) throws DocumentException, IOException {// 创建文档Document document = new Document();// 创建PdfWriter实例PdfWriter.getInstance(document, new FileOutputStream(outputPath));// 打开文档document.open();// 添加内容document.add(new Paragraph("Hello World! 这是我用iText生成的第一个PDF文档。"));document.add(new Paragraph("PDF生成时间: " + new java.util.Date()));// 关闭文档document.close();System.out.println("PDF已创建: " + outputPath);}
}
添加表格
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;import java.io.FileOutputStream;
import java.io.IOException;@Service
public class PdfTableService {public void generatePdfWithTable(String outputPath) throws DocumentException, IOException {Document document = new Document();PdfWriter.getInstance(document, new FileOutputStream(outputPath));document.open();// 添加一个段落document.add(new Paragraph("用户数据表"));// 创建表格(3列)PdfPTable table = new PdfPTable(3);// 设置表格宽度百分比table.setWidthPercentage(100);// 设置列宽比例table.setWidths(new float[]{2, 5, 3});// 添加表头addTableHeader(table);// 添加行数据addTableRows(table);// 将表格添加到文档document.add(table);document.close();}private void addTableHeader(PdfPTable table) {PdfPCell header = new PdfPCell();header.setBackgroundColor(BaseColor.LIGHT_GRAY);header.setBorderWidth(2);header.setHorizontalAlignment(Element.ALIGN_CENTER);header.setPhrase(new Phrase("ID"));table.addCell(header);header.setPhrase(new Phrase("姓名"));table.addCell(header);header.setPhrase(new Phrase("角色"));table.addCell(header);}private void addTableRows(PdfPTable table) {// 第一行table.addCell("1001");table.addCell("张三");table.addCell("管理员");// 第二行table.addCell("1002");table.addCell("李四");table.addCell("用户");// 第三行table.addCell("1003");table.addCell("王五");table.addCell("审核员");}
}
添加图片
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Image;
import com.itextpdf.text.pdf.PdfWriter;import java.io.FileOutputStream;
import java.io.IOException;@Service
public class PdfImageService {public void generatePdfWithImage(String outputPath, String imagePath) throws DocumentException, IOException {Document document = new Document();PdfWriter.getInstance(document, new FileOutputStream(outputPath));document.open();// 添加文本document.add(new Paragraph("包含图片的PDF文档"));// 添加图片Image image = Image.getInstance(imagePath);// 缩放图片image.scaleToFit(400, 300);// 设置图片位置(居中)image.setAlignment(Image.MIDDLE);document.add(image);// 在图片下添加说明document.add(new Paragraph("图1: 示例图片"));document.close();}
}
3.2 使用Apache PDFBox生成PDF
基本文档生成
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType0Font;import java.io.File;
import java.io.IOException;@Service
public class PdfBoxService {public void generateSimplePdf(String outputPath) throws IOException {// 创建新文档PDDocument document = new PDDocument();// 添加空白页PDPage page = new PDPage();document.addPage(page);// 创建内容流以添加内容PDPageContentStream contentStream = new PDPageContentStream(document, page);// 开始文本操作contentStream.beginText();// 设置字体和大小// 使用带有中文支持的字体PDType0Font font = PDType0Font.load(document, new File("src/main/resources/fonts/SimSun.ttf"));contentStream.setFont(font, 12);// 设置文本位置(从页面左下角计算,单位是点)contentStream.newLineAtOffset(25, 700);// 添加文本contentStream.showText("Hello World! 这是我用PDFBox生成的PDF文档。");// 移动到下一行contentStream.newLineAtOffset(0, -15);contentStream.showText("PDF生成时间: " + new java.util.Date());// 结束文本操作contentStream.endText();// 关闭内容流contentStream.close();// 保存文档document.save(outputPath);// 关闭文档document.close();System.out.println("PDFBox已创建PDF: " + outputPath);}
}
添加表格(PDFBox中较为复杂)
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType0Font;import java.io.File;
import java.io.IOException;@Service
public class PdfBoxTableService {public void generatePdfWithTable(String outputPath) throws IOException {// 创建文档PDDocument document = new PDDocument();PDPage page = new PDPage();document.addPage(page);PDPageContentStream contentStream = new PDPageContentStream(document, page);// 加载字体PDType0Font font = PDType0Font.load(document, new File("src/main/resources/fonts/SimSun.ttf"));// 表格内容String[][] content = {{"ID", "姓名", "角色"},{"1001", "张三", "管理员"},{"1002", "李四", "用户"},{"1003", "王五", "审核员"}};// 表格位置和尺寸float margin = 50;float y = page.getMediaBox().getHeight() - margin;float tableWidth = page.getMediaBox().getWidth() - 2 * margin;// 绘制标题contentStream.beginText();contentStream.setFont(font, 16);contentStream.newLineAtOffset(margin, y);contentStream.showText("用户数据表");contentStream.endText();y -= 30;// 计算每列宽度final int rows = content.length;final int cols = content[0].length;final float rowHeight = 20f;final float tableHeight = rowHeight * rows;final float colWidth = tableWidth / (float)cols;// 画表格// 表格外框contentStream.setLineWidth(1f);contentStream.addRect(margin, y - tableHeight, tableWidth, tableHeight);contentStream.stroke();// 画横线for(int i = 0; i < rows; i++) {contentStream.addLine(margin, y - i * rowHeight, margin + tableWidth, y - i * rowHeight);}contentStream.stroke();// 画竖线for(int i = 0; i <= cols; i++) {contentStream.addLine(margin + i * colWidth, y, margin + i * colWidth, y - tableHeight);}contentStream.stroke();// 添加文本contentStream.setFont(font, 12);// 表头使用粗体float textx = margin + 5;float texty = y - 15;for(int i = 0; i < rows; i++) {for(int j = 0; j < cols; j++) {contentStream.beginText();contentStream.newLineAtOffset(textx + j * colWidth, texty - i * rowHeight);contentStream.showText(content[i][j]);contentStream.endText();}}contentStream.close();document.save(outputPath);document.close();}
}
3.3 使用OpenPDF生成PDF
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Paragraph;
import com.lowagie.text.pdf.PdfWriter;import java.io.FileOutputStream;
import java.io.IOException;@Service
public class OpenPdfService {public void generateSimplePdf(String outputPath) throws DocumentException, IOException {// 创建文档Document document = new Document();// 创建WriterPdfWriter.getInstance(document, new FileOutputStream(outputPath));// 打开文档document.open();// 添加内容document.add(new Paragraph("Hello World! 这是我用OpenPDF生成的文档。"));document.add(new Paragraph("PDF生成时间: " + new java.util.Date()));// 关闭文档document.close();System.out.println("OpenPDF已创建PDF: " + outputPath);}
}
3.4 在SpringBoot控制器中生成并下载PDF
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.io.ByteArrayOutputStream;@RestController
@RequestMapping("/api/pdf")
public class PdfController {@Autowiredprivate PdfGenerationService pdfService;@GetMapping("/download")public ResponseEntity<byte[]> downloadPdf() {try {// 使用ByteArrayOutputStream而非文件ByteArrayOutputStream baos = new ByteArrayOutputStream();// 生成PDF到内存流pdfService.generatePdf(baos);// 设置HTTP头HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_PDF);// 设置文件下载头String filename = "generated_document.pdf";headers.setContentDispositionFormData("attachment", filename);// 返回PDF字节数组return new ResponseEntity<>(baos.toByteArray(), headers, HttpStatus.OK);} catch (Exception e) {e.printStackTrace();return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);}}
}
对应的服务类:
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.pdf.PdfWriter;import java.io.IOException;
import java.io.OutputStream;@Service
public class PdfGenerationService {public void generatePdf(OutputStream outputStream) throws DocumentException, IOException {// 创建文档Document document = new Document();// 写入输出流PdfWriter.getInstance(document, outputStream);// 打开文档document.open();// 添加内容document.add(new Paragraph("动态生成的PDF内容"));document.add(new Paragraph("此PDF由SpringBoot应用程序生成"));document.add(new Paragraph("生成时间: " + new java.util.Date()));// 关闭文档document.close();}
}
4. 读取与解析PDF
4.1 使用PDFBox读取PDF文本
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;import java.io.File;
import java.io.IOException;@Service
public class PdfReaderService {public String extractTextFromPdf(String pdfPath) throws IOException {// 加载PDF文档File file = new File(pdfPath);PDDocument document = PDDocument.load(file);try {// 创建PDF文本提取器PDFTextStripper stripper = new PDFTextStripper();// 获取文本内容String text = stripper.getText(document);return text;} finally {// 确保文档关闭if (document != null) {document.close();}}}// 提取特定页面的文本public String extractTextFromPage(String pdfPath, int pageNumber) throws IOException {File file = new File(pdfPath);PDDocument document = PDDocument.load(file);try {PDFTextStripper stripper = new PDFTextStripper();// 设置起始页和结束页stripper.setStartPage(pageNumber);stripper.setEndPage(pageNumber);return stripper.getText(document);} finally {if (document != null) {document.close();}}}
}
4.2 使用iText解析PDF
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.parser.PdfTextExtractor;
import com.itextpdf.text.pdf.parser.SimpleTextExtractionStrategy;
import com.itextpdf.text.pdf.parser.TextExtractionStrategy;import java.io.IOException;@Service
public class ITextPdfReaderService {public String extractTextFromPdf(String pdfPath) throws IOException {PdfReader reader = new PdfReader(pdfPath);StringBuilder textBuilder = new StringBuilder();try {int pages = reader.getNumberOfPages();// 遍历所有页面for (int i = 1; i <= pages; i++) {TextExtractionStrategy strategy = new SimpleTextExtractionStrategy();String pageText = PdfTextExtractor.getTextFromPage(reader, i, strategy);textBuilder.append(pageText).append("\n");}return textBuilder.toString();} finally {if (reader != null) {reader.close();}}}// 获取PDF元数据public Map<String, String> getPdfMetadata(String pdfPath) throws IOException {PdfReader reader = new PdfReader(pdfPath);Map<String, String> metadata = new HashMap<>();try {metadata.put("Title", reader.getInfo().get("Title"));metadata.put("Author", reader.getInfo().get("Author"));metadata.put("Subject", reader.getInfo().get("Subject"));metadata.put("Keywords", reader.getInfo().get("Keywords"));metadata.put("Creator", reader.getInfo().get("Creator"));metadata.put("Producer", reader.getInfo().get("Producer"));metadata.put("Creation Date", reader.getInfo().get("CreationDate"));metadata.put("Modification Date", reader.getInfo().get("ModDate"));metadata.put("Page Count", String.valueOf(reader.getNumberOfPages()));return metadata;} finally {if (reader != null) {reader.close();}}}
}
4.3 从PDF中提取表格数据
提取PDF中的表格是一个复杂任务,可以使用如Tabula-Java等专门库:
<dependency><groupId>technology.tabula</groupId><artifactId>tabula</artifactId><version>1.0.5</version>
</dependency>
import technology.tabula.ObjectExtractor;
import technology.tabula.Page;
import technology.tabula.PageIterator;
import technology.tabula.Rectangle;
import technology.tabula.Table;
import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;@Service
public class PdfTableExtractorService {public List<String[][]> extractTablesFromPdf(String pdfPath) throws IOException {// 打开PDF文件PDDocument document = PDDocument.load(new File(pdfPath));List<String[][]> allTables = new ArrayList<>();try {// 创建ObjectExtractorObjectExtractor extractor = new ObjectExtractor(document);// 提取所有页面PageIterator iterator = extractor.extract();// 表格提取算法SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm();// 处理每一页while (iterator.hasNext()) {Page page = iterator.next();// 提取表格List<Table> tables = sea.extract(page);// 处理每个表格for (Table table : tables) {int rowCount = table.getRowCount();int colCount = table.getColCount();String[][] tableData = new String[rowCount][colCount];// 提取单元格数据for (int i = 0; i < rowCount; i++) {for (int j = 0; j < colCount; j++) {if (j < table.getRows().get(i).size()) {tableData[i][j] = table.getRows().get(i).get(j).getText();} else {tableData[i][j] = "";}}}allTables.add(tableData);}}} finally {if (document != null) {document.close();}}return allTables;}// 打印表格数据(用于测试)public void printTableData(String[][] tableData) {for (String[] row : tableData) {for (String cell : row) {System.out.print(cell + " | ");}System.out.println();}}
}
5. 修改现有PDF文件
修改现有PDF文件是常见需求,包括添加新内容、修改文本、删除页面、合并文档等操作。
5.1 添加水印和页码
使用iText添加水印
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Document;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.FontFactory;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfGState;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;import java.io.FileOutputStream;
import java.io.IOException;@Service
public class PdfWatermarkService {public void addWatermark(String inputPath, String outputPath, String watermarkText) throws IOException, DocumentException {// 打开现有PDFPdfReader reader = new PdfReader(inputPath);PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(outputPath));// 创建基本字体BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);Font font = new Font(baseFont, 30, Font.BOLD, BaseColor.GRAY);// 获取PDF页数int pageCount = reader.getNumberOfPages();// 对每页添加水印for (int i = 1; i <= pageCount; i++) {// 获取页面尺寸Rectangle pageRect = reader.getPageSize(i);float width = pageRect.getWidth();float height = pageRect.getHeight();// 获取内容字节层(在内容下方)PdfContentByte under = stamper.getUnderContent(i);// 设置透明度PdfGState gs = new PdfGState();gs.setFillOpacity(0.3f);under.setGState(gs);// 保存图形状态under.saveState();// 设置字体和颜色under.setFontAndSize(baseFont, 30);under.setColorFill(BaseColor.GRAY);// 添加水印文本(旋转45度)under.beginText();// 文本旋转和位置under.showTextAligned(Element.ALIGN_CENTER, watermarkText, width / 2, height / 2, 45);under.endText();// 恢复图形状态under.restoreState();}// 关闭资源stamper.close();reader.close();System.out.println("水印已添加: " + outputPath);}
}
使用PDFBox添加页码
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
import org.apache.pdfbox.pdmodel.font.PDType1Font;import java.io.IOException;@Service
public class