资讯动态

OFD与PDF互转及SM2签名实现方案

发布时间:2026/10/27 6:15:55 来源:尧图企业网站定制
1. 项目概述在政务和金融领域OFDOpen Fixed-layout Document作为我国自主制定的版式文档标准正逐步替代PDF成为电子发票、电子公文等场景的首选格式。而SM2作为国密算法的重要组成部分在电子签章领域有着广泛应用。最近我在一个政务项目中遇到了这样的需求系统需要同时处理PDF和OFD文件并实现SM2算法对OFD文档的数字签名。通过对比多个开源方案最终选择了ofdrw这个国产开源库。它不仅支持OFD与PDF的相互转换还内置了完整的SM2签名功能。更重要的是它采用纯Java实现与SpringBoot的集成非常顺畅。下面我就详细分享这套方案的实现过程。2. 环境准备与依赖配置2.1 基础环境要求JDK 1.8推荐JDK11Maven 3.6SpringBoot 2.5.x实测兼容2.3-2.7各版本注意ofdrw某些高级功能需要BouncyCastle加密库支持建议提前确认环境没有加密策略限制2.2 Maven依赖配置在pom.xml中添加以下核心依赖!-- ofdrw核心库 -- dependency groupIdorg.ofdrw/groupId artifactIdofdrw-full/artifactId version2.1.5/version /dependency !-- PDFBox用于PDF处理 -- dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.26/version /dependency !-- BouncyCastle加密支持 -- dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15on/artifactId version1.69/version /dependency2.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 ResponseEntityResource 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 ResponseEntityResource 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 ResponseEntityResource 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 ResponseEntityResource 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 性能优化方案文件缓存策略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 { // 转换逻辑 } }异步处理改造Async Cacheable(value pdf2ofd, key #pdfFile.hashCode()) public CompletableFutureFile convertAsync(File pdfFile) { return CompletableFuture.completedFuture(convert(pdfFile)); }批量处理优化public ListFile batchConvert(ListFile 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建议先转换为图片再插入OFD5.3 签名异常排查签名位置偏移// 精确控制签名位置 appearance.setSignRect(50, 100, 80, 80) // x,y,width,height .setPageIndex(0); // 指定页码签名不显示检查OFD阅读器是否支持数字签名渲染确认签名外观设置有效性能优化// 对大文件使用分段处理 new OFDSigner(reader, writer) .setSegmentSize(1024*1024) // 1MB分段 .sign();6. 扩展功能实现6.1 OFD文档合并public File mergeOfds(ListFile 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的安全特性完美契合了政务系统的合规要求。一个关键经验是对于批量处理场景一定要做好文件缓存和异步处理否则系统负载会急剧上升。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑