
1. 为什么我们需要在SpringBoot中实现PDF水印功能PDF文档作为现代办公和知识传播的重要载体其安全性和版权保护日益受到重视。在我参与过的一个企业文档管理系统项目中客户明确要求所有对外分发的PDF文件必须包含公司标识和机密字样这就是典型的PDF水印应用场景。PDF水印主要解决三大问题版权声明防止文档被非法复制或篡改比如电子书、培训资料状态标识标注文档状态如草稿、审批中、最终版溯源追踪添加用户ID或时间戳便于追踪文档泄露源头在SpringBoot项目中集成PDF水印功能时我们需要考虑几个关键因素水印的清晰度与文档可读性的平衡水印的抗去除性不能简单通过截图去除批量处理时的性能消耗水印位置的自适应不同尺寸页面提示生产环境中建议将水印生成逻辑封装为独立服务通过消息队列异步处理大文件避免阻塞主业务流程。2. Apache PDFBox方案实现详解2.1 环境准备与依赖配置PDFBox是Apache旗下的开源PDF处理库其2.0版本重写了大部分API性能提升显著。在SpringBoot项目中引入dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.27/version /dependency建议同时添加字体库依赖解决中文乱码问题dependency groupIdorg.apache.pdfbox/groupId artifactIdfontbox/artifactId version2.0.27/version /dependency2.2 基础文本水印实现以下是带详细注释的核心代码实现public void addTextWatermark(File inputFile, File outputFile, String watermarkText) throws IOException { // 1. 加载PDF文档 try (PDDocument document PDDocument.load(inputFile)) { // 2. 创建字体对象支持中文 PDFont font PDType1Font.HELVETICA_BOLD; // 中文需使用TTF字体 // PDFont font PDType0Font.load(document, new File(simhei.ttf)); // 3. 遍历所有页面 for (PDPage page : document.getPages()) { // 4. 获取页面尺寸 PDRectangle pageSize page.getMediaBox(); float pageWidth pageSize.getWidth(); float pageHeight pageSize.getHeight(); // 5. 创建内容流APPEND模式保留原内容 try (PDPageContentStream contentStream new PDPageContentStream( document, page, PDPageContentStream.AppendMode.APPEND, true, true)) { // 6. 设置字体和透明度 contentStream.setFont(font, 48); contentStream.setNonStrokingColor(200, 200, 200); // 浅灰色 // 7. 计算水印位置居中倾斜 AffineTransform transform new AffineTransform(); transform.translate(pageWidth/2, pageHeight/2); transform.rotate(Math.toRadians(45)); // 45度倾斜 contentStream.setTextMatrix(transform); // 8. 添加文本 contentStream.beginText(); contentStream.showText(watermarkText); contentStream.endText(); } } // 9. 保存文档 document.save(outputFile); } }2.3 高级水印技巧多行水印实现// 在内容流中添加以下代码 float textWidth font.getStringWidth(watermarkText) / 1000 * 48; float textHeight font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * 48; for (int i 0; i 5; i) { for (int j 0; j 3; j) { AffineTransform tx new AffineTransform(); tx.translate(pageWidth * 0.2 * i, pageHeight * 0.3 * j); tx.rotate(Math.toRadians(30)); contentStream.setTextMatrix(tx); contentStream.beginText(); contentStream.showText(watermarkText); contentStream.endText(); } }图片水印实现// 加载图片 PDImageXObject pdImage PDImageXObject.createFromFile(logo.png, document); // 计算缩放比例 float scale 0.2f; // 缩放20% float imageWidth pdImage.getWidth() * scale; float imageHeight pdImage.getHeight() * scale; // 添加图片 contentStream.drawImage(pdImage, (pageWidth - imageWidth)/2, // 居中 (pageHeight - imageHeight)/2, imageWidth, imageHeight);3. iText方案实现与对比3.1 iText与PDFBox的选型对比特性PDFBoxiText开源协议Apache 2.0AGPL/商业授权中文支持需额外配置字体内置更好支持性能中等较高水印灵活性一般非常灵活学习曲线平缓较陡峭适合场景简单水印需求复杂版式需求3.2 iText核心实现代码public void addWatermarkWithIText(String src, String dest, String watermark) throws IOException, DocumentException { PdfReader reader new PdfReader(src); PdfStamper stamper new PdfStamper(reader, new FileOutputStream(dest)); // 使用中文字体 BaseFont baseFont BaseFont.createFont( STSong-Light, UniGB-UCS2-H, BaseFont.EMBEDDED); // 获取总页数 int pageCount reader.getNumberOfPages(); // 水印图层OVER_CONTENT在上层UNDER_CONTENT在下层 PdfContentByte content; PdfGState gs new PdfGState(); gs.setFillOpacity(0.3f); // 设置透明度 // 遍历所有页面 for (int i 1; i pageCount; i) { // 获取页面尺寸 Rectangle pageSize reader.getPageSize(i); float x pageSize.getWidth() / 2; float y pageSize.getHeight() / 2; content stamper.getOverContent(i); content.saveState(); content.setGState(gs); content.beginText(); content.setFontAndSize(baseFont, 48); content.setColorFill(BaseColor.LIGHT_GRAY); // 添加倾斜文字 content.showTextAligned( Element.ALIGN_CENTER, watermark, x, y, 45); // 45度角 content.endText(); content.restoreState(); } stamper.close(); reader.close(); }3.3 iText高级特性动态水印带日期和用户信息String dynamicWatermark String.format( %s\n%s\n%s, 机密文档, new SimpleDateFormat(yyyy-MM-dd).format(new Date()), 授权用户: username); content.showTextAligned( Element.ALIGN_CENTER, dynamicWatermark, x, y, 45);防复制水印// 设置不可复制 stamper.setEncryption( null, null, PdfWriter.ALLOW_PRINTING | PdfWriter.ALLOW_SCREENREADERS, PdfWriter.STANDARD_ENCRYPTION_128);4. 生产环境最佳实践4.1 性能优化方案批量处理优化// 使用线程池处理多个文件 ExecutorService executor Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors() * 2); ListFuture? futures new ArrayList(); for (File pdf : pdfFiles) { futures.add(executor.submit(() - { addWatermark(pdf, new File(outputDir, pdf.getName())); })); } // 等待所有任务完成 for (Future? future : futures) { future.get(); } executor.shutdown();内存优化技巧// PDFBox内存设置 System.setProperty(org.apache.pdfbox.baseParser.pushBackSize, 1000000); System.setProperty(org.apache.pdfbox.rendering.UsePureJavaCMYKConversion, true); // iText内存优化 ReaderProperties props new ReaderProperties() .setMemoryLimit(50); // MB PdfReader reader new PdfReader(src, props);4.2 常见问题排查中文乱码问题PDFBox必须加载中文字体文件如simhei.ttfiText使用STSong-Light等支持中文的字体水印不显示检查图层顺序OVER_CONTENT/UNDER_CONTENT验证透明度设置是否过低建议0.2-0.5确认文件没有加密或权限限制性能瓶颈大文件处理时出现OOM增加JVM内存-Xmx1024m分页处理不要一次性加载整个文档处理速度慢关闭PDFBox的校验document.setStrictParsing(false)使用iText的智能模式reader.setMemorySavingMode(true)4.3 SpringBoot集成方案REST接口实现RestController RequestMapping(/api/pdf) public class PdfController { PostMapping(/watermark) public ResponseEntityResource addWatermark( RequestParam MultipartFile file, RequestParam String text, RequestParam(defaultValue 30) float opacity) throws IOException { // 临时文件处理 File inputFile File.createTempFile(input-, .pdf); file.transferTo(inputFile); File outputFile File.createTempFile(output-, .pdf); // 调用水印服务 watermarkService.addTextWatermark(inputFile, outputFile, text, opacity); // 返回流 Path path Paths.get(outputFile.getAbsolutePath()); ByteArrayResource resource new ByteArrayResource(Files.readAllBytes(path)); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, attachment; filenamewatermarked.pdf) .contentType(MediaType.APPLICATION_PDF) .contentLength(outputFile.length()) .body(resource); } }异步处理方案Service public class PdfWatermarkService { Async public CompletableFutureFile asyncAddWatermark(File input, String text) { File output new File(input.getParent(), watermarked_ input.getName()); // 水印处理逻辑... return CompletableFuture.completedFuture(output); } }在实际项目中我们还需要考虑文件上传大小限制spring.servlet.multipart.max-file-size水印模板管理数据库存储常用水印样式水印位置预设页眉、页脚、对角等日志记录与审计追踪