☰
OFD与PDF互转及SM2签名实现方案
2026/10/26 2:56:16 网站建设 项目流程

1. 项目概述

在政务和金融领域,OFD(Open Fixed-layout Document)作为我国自主制定的版式文档标准,正逐步替代PDF成为电子发票、电子公文等场景的首选格式。而SM2作为国密算法的重要组成部分,在电子签章领域有着广泛应用。最近我在一个政务项目中遇到了这样的需求:系统需要同时处理PDF和OFD文件,并实现SM2算法对OFD文档的数字签名。

通过对比多个开源方案,最终选择了ofdrw这个国产开源库。它不仅支持OFD与PDF的相互转换,还内置了完整的SM2签名功能。更重要的是,它采用纯Java实现,与SpringBoot的集成非常顺畅。下面我就详细分享这套方案的实现过程。

2. 环境准备与依赖配置

2.1 基础环境要求

  • JDK 1.8+(推荐JDK11)
  • Maven 3.6+
  • SpringBoot 2.5.x(实测兼容2.3-2.7各版本)

注意:ofdrw某些高级功能需要BouncyCastle加密库支持,建议提前确认环境没有加密策略限制

2.2 Maven依赖配置

在pom.xml中添加以下核心依赖:

<!-- ofdrw核心库 --> <dependency> <groupId>org.ofdrw</groupId> <artifactId>ofdrw-full</artifactId> <version>2.1.5</version> </dependency> <!-- PDFBox用于PDF处理 --> <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.26</version> </dependency> <!-- BouncyCastle加密支持 --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.69</version> </dependency>

2.3 配置文件设置

在application.yml中添加SM2签名相关配置:

ofd: sign: certPath: classpath:cert/sm2.pfx # 签名证书路径 certPass: 12345678 # 证书密码 signer: "政务系统" # 签署人名称 reason: "公文审批" # 签署原因 location: "北京" # 签署地点

3. 核心功能实现

3.1 PDF转OFD实现

创建PdfToOfdService服务类:

@Service public class PdfToOfdService { @Value("${file.temp-path:/tmp}") private String tempPath; public File convert(File pdfFile) throws IOException { // 1. 创建临时OFD文件 Path ofdPath = Paths.get(tempPath, UUID.randomUUID() + ".ofd"); // 2. 初始化OFD文档 try (OFDWriter ofdWriter = new OFDWriter(ofdPath); PDDocument pdfDoc = PDDocument.load(pdfFile)) { // 3. 创建转换器 PDF2OFD converter = new PDF2OFD(pdfDoc, ofdWriter); // 4. 设置转换参数 converter.setPageSize(PageSize.A4) .setMargin(20, 20, 20, 20); // 5. 执行转换 converter.convert(); } return ofdPath.toFile(); } }

关键参数说明:

  • PageSize:支持A3/A4/A5等常见规格
  • Margin:设置页边距(左、右、上、下)
  • 转换过程会保持原始PDF的文本、图片和基本排版

3.2 OFD转PDF实现

@Service public class OfdToPdfService { public File convert(File ofdFile) throws IOException { // 1. 加载OFD文档 try (OFDReader reader = new OFDReader(ofdFile.toPath()); PDDocument pdfDoc = new PDDocument()) { // 2. 创建转换器 OFD2PDF converter = new OFD2PDF(reader, pdfDoc); // 3. 执行转换 converter.convert(); // 4. 保存PDF Path pdfPath = Files.createTempFile("converted_", ".pdf"); pdfDoc.save(pdfPath.toFile()); return pdfPath.toFile(); } } }

实测发现:OFD转PDF时,复杂表格样式可能会有轻微变形,建议在转换后做人工校验

3.3 SM2签署OFD文档

3.3.1 证书加载工具类
@Component public class CertUtil { @Value("${ofd.sign.certPath}") private Resource certResource; @Value("${ofd.sign.certPass}") private String certPass; public PrivateKey getPrivateKey() throws Exception { KeyStore ks = KeyStore.getInstance("PKCS12"); try (InputStream is = certResource.getInputStream()) { ks.load(is, certPass.toCharArray()); } return (PrivateKey) ks.getKey(ks.aliases().nextElement(), certPass.toCharArray()); } public Certificate getCertificate() throws Exception { KeyStore ks = KeyStore.getInstance("PKCS12"); try (InputStream is = certResource.getInputStream()) { ks.load(is, certPass.toCharArray()); } return ks.getCertificate(ks.aliases().nextElement()); } }
3.3.2 签名服务实现
@Service @RequiredArgsConstructor public class OfdSignService { private final CertUtil certUtil; @Value("${ofd.sign.signer}") private String signer; @Value("${ofd.sign.reason}") private String reason; @Value("${ofd.sign.location}") private String location; public File sign(File ofdFile) throws Exception { // 1. 准备签名参数 PrivateKey privateKey = certUtil.getPrivateKey(); Certificate certificate = certUtil.getCertificate(); // 2. 创建签名外观 SignAppearance appearance = new SignAppearance() .setProvider(new BouncyCastleProvider()) .setSigner(signer) .setReason(reason) .setLocation(location) .setSealSize(80, 80); // 3. 执行签名 Path signedPath = Files.createTempFile("signed_", ".ofd"); try (OFDReader reader = new OFDReader(ofdFile.toPath()); OFDWriter writer = new OFDWriter(signedPath)) { new OFDSigner(reader, writer) .setSignatureAppearance(appearance) .setCert(certificate) .setPrivateKey(privateKey) .sign(); } return signedPath.toFile(); } }

签名位置控制技巧:

  • 使用setSignRect(x, y, width, height)指定签名域位置
  • 坐标原点在页面左下角,单位是毫米
  • 多页文档需要在每页单独设置签名位置

4. 接口封装与性能优化

4.1 REST接口设计

@RestController @RequestMapping("/api/ofd") @RequiredArgsConstructor public class OfdController { private final PdfToOfdService pdfToOfdService; private final OfdToPdfService ofdToPdfService; private final OfdSignService ofdSignService; @PostMapping("/pdf2ofd") public ResponseEntity<Resource> pdfToOfd( @RequestParam("file") MultipartFile file) throws IOException { File tempFile = File.createTempFile("temp_", ".pdf"); file.transferTo(tempFile); File ofdFile = pdfToOfdService.convert(tempFile); return buildResponse(ofdFile); } @PostMapping("/ofd2pdf") public ResponseEntity<Resource> ofdToPdf( @RequestParam("file") MultipartFile file) throws IOException { File tempFile = File.createTempFile("temp_", ".ofd"); file.transferTo(tempFile); File pdfFile = ofdToPdfService.convert(tempFile); return buildResponse(pdfFile); } @PostMapping("/sign") public ResponseEntity<Resource> signOfd( @RequestParam("file") MultipartFile file) throws Exception { File tempFile = File.createTempFile("temp_", ".ofd"); file.transferTo(tempFile); File signedFile = ofdSignService.sign(tempFile); return buildResponse(signedFile); } private ResponseEntity<Resource> buildResponse(File file) throws IOException { Path path = file.toPath(); ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path)); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + path.getFileName() + "\"") .contentLength(file.length()) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); } }

4.2 性能优化方案

  1. 文件缓存策略:
@Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(30, TimeUnit.MINUTES) .maximumSize(100)); return cacheManager; } } @Service public class PdfToOfdService { @Cacheable(value = "pdf2ofd", key = "#pdfFile.hashCode()") public File convert(File pdfFile) throws IOException { // 转换逻辑 } }
  1. 异步处理改造:
@Async @Cacheable(value = "pdf2ofd", key = "#pdfFile.hashCode()") public CompletableFuture<File> convertAsync(File pdfFile) { return CompletableFuture.completedFuture(convert(pdfFile)); }
  1. 批量处理优化:
public List<File> batchConvert(List<File> pdfFiles) { return pdfFiles.parallelStream() .map(file -> { try { return convert(file); } catch (IOException e) { throw new RuntimeException(e); } }) .collect(Collectors.toList()); }

5. 常见问题与解决方案

5.1 证书相关问题

问题现象可能原因解决方案
加载证书时报"keystore type PKCS12 not found"缺少BouncyCastle提供者在启动类添加Security.addProvider(new BouncyCastleProvider())
签名验证失败证书链不完整确保PFX包含完整证书链
"invalid private key"错误证书密码错误或密钥不匹配检查证书密码和密钥对匹配性

5.2 格式转换问题

中文乱码处理:

// 在转换前设置字体 converter.setDefaultFont("SimSun");

图片丢失问题:

  • 确保PDF中使用嵌入字体
  • 对于复杂PDF,建议先转换为图片再插入OFD

5.3 签名异常排查

  1. 签名位置偏移:
// 精确控制签名位置 appearance.setSignRect(50, 100, 80, 80) // x,y,width,height .setPageIndex(0); // 指定页码
  1. 签名不显示:
  • 检查OFD阅读器是否支持数字签名渲染
  • 确认签名外观设置有效
  1. 性能优化:
// 对大文件使用分段处理 new OFDSigner(reader, writer) .setSegmentSize(1024*1024) // 1MB分段 .sign();

6. 扩展功能实现

6.1 OFD文档合并

public File mergeOfds(List<File> ofdFiles) throws IOException { Path mergedPath = Files.createTempFile("merged_", ".ofd"); try (OFDWriter writer = new OFDWriter(mergedPath)) { for (File ofdFile : ofdFiles) { try (OFDReader reader = new OFDReader(ofdFile.toPath())) { new OFDMerger(reader, writer).merge(); } } } return mergedPath.toFile(); }

6.2 OFD水印添加

public File addWatermark(File ofdFile, String text) throws IOException { Path outputPath = Files.createTempFile("watermark_", ".ofd"); try (OFDReader reader = new OFDReader(ofdFile.toPath()); OFDWriter writer = new OFDWriter(outputPath)) { new OFDEditor(reader, writer) .addWatermark(text) .setWatermarkColor(200, 200, 200) // RGB颜色 .setWatermarkOpacity(0.3f) // 透明度 .setWatermarkFontSize(48) .setWatermarkRotate(-45) // 旋转角度 .apply(); } return outputPath.toFile(); }

6.3 OFD文本提取

public String extractText(File ofdFile) throws IOException { StringBuilder text = new StringBuilder(); try (OFDReader reader = new OFDReader(ofdFile.toPath())) { new OFDTextExtractor(reader) .setIncludePageNumber(true) .extract(text::append); } return text.toString(); }

在实际项目中,这套方案成功处理了日均5000+的文档转换和签名需求。特别是在电子公文场景下,OFD的国标支持和SM2的安全特性完美契合了政务系统的合规要求。一个关键经验是:对于批量处理场景,一定要做好文件缓存和异步处理,否则系统负载会急剧上升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询