SpringBoot实战(三十二)集成 ofdrw,实现 PDF 和 OFD 的转换、SM2 签署OFD
目录
- 一、OFD 简介
- 1.1 什么是 OFD?
- 1.2 什么是 版式文档?
- 1.3 为什么要用 OFD 而不是PDF?
- 二、ofdrw 简介
- 2.1 定义
- 2.2 Maven 依赖
- 2.3 ofdrw 的 13 个模块
- 三、PDF/文本/图片 转 OFD(ofdrw-conterver)
- 3.1 介绍:
- 3.2 Maven 依赖:
- 3.3 PDF转换OFD
- 3.5 文本转换OFD
- 3.6 图片转换OFD
- 四、OFD 转 图片/HTML/文本/PDF(ofdrw-conterver)
- 4.1 介绍:
- 4.2 Maven 依赖:
- 4.3 导出为图片
- 4.4 导出为SVG图形
- 4.5 导出为HTML网页
- 4.6 导出为文本
- 4.7 [不推荐] 导出为PDF
- 五、OFD签署
- 5.1 在线生成 sm2 证书
- 5.2 制作 esl 印章
- 5.3 坐标签署OFD
- 5.4 骑缝签署OFD
- 5.5 验签 OFD
- OFDRW-Gitee地址: https://gitee.com/ofdrw/ofdrw
- OFDRW-GitHub地址: https://github.com/ofdrw/ofdrw
- gmhelper-GitHub地址: https://github.com/ZZMarquis/gmhelper
一、OFD 简介
1.1 什么是 OFD?
OFD
是开放版式文档(Open Fixed-layout Document)
的英文缩写,是我国国家版式文档格式标准——《GB/T 33190-2016电子文件存储与交换格式-版式文档》。
1.2 什么是 版式文档?
版式文档
是与Word(doc、docx)
等流式文件
相对的,具有格式独立、版本固定、固化呈现的文档。版式文档不宜修改,且在不同设备中显示效果不变,而 流式文档会根据设备版面显示发生变化。
举例来说:
一个 doc 格式的 Word 文档,使用 Word 与 WPS 打开,容易发生版面(样式)变化、内容重排现象,同一篇 Word 文档,在 Office 的不同版本中打开也会发生不一致的情况。而版式文档则是不受设备影响,版式固定。在版式、版面、字体、字号等方面与纸质文档保持完全一致。版式文档格式的特点使它称为 严肃类电子文档 发布、数字化信息传播和存档的理想文档格式。
版式文档的代表就是我们工作和生活中非常熟悉的 PDF
文档,OFD
文档则是 我国自主研发,自主指定的版式文件格式标准。在 PDF基础上加入了许多基于我国社会发展需要的应用场景功能。可以说 OFD 与 PDF 定位一致,同为版式文档格式,但 OFD 后发制人,青出于蓝。
1.3 为什么要用 OFD 而不是PDF?
既然 PDF 和 OFD 都是版式文件,那么问题来了,PDF 用得好好的,我们为什么要自己再做一个 OFD 的文件格式呢?
主要出于以下几点原因:
-
格式不统一: 目前在国内可使用的版式文件格式包括
PDF
、CEB
等在内有不下十种,来自不同厂商和不同的技术,没有统一标准,就会导致不同机构、不同企业之间的文件交流存在阻碍,文件长期存档很困难。 -
难以自主可控: 我们知道,在目前办公文档市场上,来自 Adobe 的 PDF 五一占据绝对主流,而在 PDF 阅读工具的市场上,Adobe 旗下的
Adobe Acrobat DC
又以 55.18% 的市占率位于领先地位,国内厂商 Foxit福昕
(昕,xin,一声)虽然排名第二,但占有率仅有 1.92%。这背后有一个严重的问题,就是在版式文件技术上,我们目前难以做到自主可控,如果我们相对文件做一些针对国内特殊领域的技术扩展时,极容易受制于外部厂商,或者如果未来 Adobe 停止技术授权,那可能有很多文档遭受损失。
此外,OFD 作为我们自主研发、自主可控的版式文件格式,相比 PDF 等其他版式文件,OFD 有一些技术上的优势:
- 第一:OFD 文档内部采用可扩展标记语言
XML
来描述数据和结构,体积精简,安全开放,易于扩展。 - 第二:OFD 支持国产加密算法,具有全面的安全保障体系,可防止信息被窃取,并且和数字签名技术集合,可防篡改,更加安全。
- 第三:永久刻度可用,可对文件长久保存,且可以精准呈现,文件的版式内容在不同场景、设备下都能保持一致性。
- 第四:支持直接进行文件归档的一系列处理。
这些优势总结起来,就是:在 PDF 基础上加入了许多基于我国社会发展需要的应用场景功能。
二、ofdrw 简介
2.1 定义
ofdrw
,全称OFD Reader & Writer
,意为 OFD 读写器,是开源的 OFD 处理库,支持 文档生成、数字签名、文档保护、文档合并、转换、导出 等功能。
Gitee地址: https://gitee.com/ofdrw/ofdrw
GitHub地址: https://github.com/ofdrw/ofdrw
2.2 Maven 依赖
Maven 依赖如下:
<!-- ofdrw -->
<dependency>
<groupId>org.ofdrw</groupId>
<artifactId>ofdrw-full</artifactId>
<version>2.3.3</version>
</dependency>
2.3 ofdrw 的 13 个模块
- ofdrw-core OFD核心API,参考《GB/T 33190-2016 电子文件存储与交换格式版式文档》实现的基础数据结构。
- ofdrw-font 生成OFD字体相关。
- ofdrw-layout OFD布局引擎库,用于文档构建和渲染。
- ofdrw-pkg OFD文件的容器,用于文档的打包。
- ofdrw-reader OFD文档解析器,用于OFD的反序列化以及签名签章。
- ofdrw-sign OFD文档数字签章。
- ofdrw-gm 用于支持签章模块需要的国密电子签章数据结构。
- ofrw-crypto 用于实现《GM/T 0099-2020 开放版式文档密码应用技术规范》对OFD的密码相关功能。
- ofdrw-gv OFDRW 所有模块所共用的全局变量。
- ofdrw-converter OFD文档转换。
- ofdrw-tool OFD文档工具,文档合并、裁剪、重组。
- ofdrw-graphics2d 实现了AWT Graphics2D接口,生成OFD文档内容。
- ofdrw-full 上述所有模块整合包,用于简化依赖引入。
由于篇幅约束,这里我们只介绍 ofdrw-converter
、ofdrw-sign
两个模块对应的 OFD 转换 和 OFD 签署 两个功能。
三、PDF/文本/图片 转 OFD(ofdrw-conterver)
- 官方文档: https://gitee.com/ofdrw/ofdrw/blob/master/ofdrw-converter/doc/CONVERTER.md
3.1 介绍:
ofdrw-converter
提供了 将其它类型媒体文件或文档转换成 OFD 文档内容 的功能。ofdrw-converter 模块在2.0.0
之后开始提供其它文档或媒体类型向 OFD 文档转换功能。
核心接口类: org.ofdrw.converter.ofdconverter.DocConverter
核心方法签名:
void convert(Path filepath, int... indexes) throws GeneralConvertException;
3 个实现类:
接口实现类命名格式: 原媒体格式+Converter
- PDF文档:
PDFConverter
- 纯文本:
TextConverter
- 图片:
ImageConverter
注意:
convert()
方法的参数页码均从 0 起,例如文档中的第 1 页的 Index 也就是 0,并非所有媒体格式都有页码,在转换无页码的没给是,页码参数无效。
3.2 Maven 依赖:
<dependency>
<groupId>org.ofdrw</groupId>
<artifactId>ofdrw-converter</artifactId>
<version>2.3.3</version>
</dependency>
3.3 PDF转换OFD
将PDF中页面转换为OFD页面,采用PDFBox PDFRenderer
接口,以AWT graphics2d接口桥接,并通过ofdrw-graphics2d 模块完成转换功能。
实现类:org.ofdrw.converter.ofdconverter.PDFConverter
注意事项:
- 转换后的页面将采用PDF中页面尺寸。
- 目前该转换器任然有改进空间,可能存在部分特性在转换过程中丢失,显示效果与原PDF文档不一致。
示例:
import org.ofdrw.converter.ofdconverter.PDFConverter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* pdf 转 ofd
*/
private static void pdf2Ofd() {
Path src = Paths.get("D:\\test.pdf");
Path dst = Paths.get("D:\\test.ofd");
try (PDFConverter converter = new PDFConverter(dst)) {
converter.convert(src);
} catch (IOException e) {
log.error("pdf 转 ofd 失败,请稍后重试,原因:{}", e.getMessage(), e);
throw new RuntimeException("pdf 转 ofd 失败,请稍后重试", e);
}
System.out.println("pdf 转 ofd 成功,路径: " + dst.toAbsolutePath());
}
特有方法 | 用途 |
---|---|
void setEnableCopyAttachFiles(boolean enableCopyAttachFiles) | 设置是否复制附件(默认复制)。 |
void setEnableCopyBookmarks(boolean enableCopyBookmarks) | 设置是否复制书签(默认复制)。 |
void setUUPMM(double UUPMM) | 设置毫米表示的用户单元数(PDF单位) |
详见 测试用例
执行结果:
3.5 文本转换OFD
将文本转换为OFD。
实现类:org.ofdrw.converter.ofdconverter.TextConverter
注意事项:
- 文本文件为无格式文件,若您需要设置文本格式请使用ofdrw-layout模块。
示例:
import org.ofdrw.converter.ofdconverter.TextConverter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* text 转 ofd
*/
private static void text2Ofd() {
Path src = Paths.get("D:\\test.txt");
Path dst = Paths.get("D:\\test.ofd");
try (TextConverter converter = new TextConverter(dst)) {
converter.convert(src);
converter.convert(src);
converter.convert(src);
} catch (IOException e) {
log.error("text 转 ofd 失败,请稍后重试,原因:{}", e.getMessage(), e);
throw new RuntimeException("text 转 ofd 失败,请稍后重试", e);
}
System.out.println("text 转 ofd 成功,路径: " + dst.toAbsolutePath());
}
特有方法 | 用途 |
---|---|
void append(String txt) | 追加文本,文本将新起一行。 |
void setPageSize(PageLayout pageLayout) | 设置OFD页面尺寸。 |
void setFontSize(double fontSize) | 设置字号,单位毫米。 |
详见 测试用例
执行结果:
3.6 图片转换OFD
导入图片到OFD中,图片格式支持PNG、BPM、JPG。
实现类:org.ofdrw.converter.ofdconverter.ImageConverter
注意事项:
- 可以通过构造器指定导出的图片类型,目前支持
PNG
、JPG
、BPM
,默认为PNG
格式。- 若图片格式不在上述范围您可能需要通过手动的方式设置加入图片大小。
- 可以通过方法设置导出图片的质量,也就是
ppm
参数,默认ppm
为15(15像素1毫米)。- 每个添加的图片都将独立为一页,并且居中。
示例:
import org.ofdrw.converter.ofdconverter.ImageConverter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* img 转 ofd
*/
private static void img2Ofd() {
Path src = Paths.get("D:\\signature.png");
Path dst = Paths.get("D:\\test.ofd");
try (ImageConverter converter = new ImageConverter(dst)) {
// 可以加多个图片,每张图片一页
converter.convert(src);
converter.convert(src);
converter.convert(src);
} catch (IOException e) {
log.error("img 转 ofd 失败,请稍后重试,原因:{}", e.getMessage(), e);
throw new RuntimeException("img 转 ofd 失败,请稍后重试", e);
}
System.out.println("img 转 ofd 成功,路径: " + dst.toAbsolutePath());
}
特有方法 | 用途 |
---|---|
void setPPM(double ppm) | 设置图片质量,单位为:每毫米像素数量。 |
void append(Path filepath, double width, double height) | 追加图片到新页面并指定显示大小。 |
void setPageSize(PageLayout pageLayout) | 设置OFD页面尺寸。 |
详见 测试用例
执行结果:
四、OFD 转 图片/HTML/文本/PDF(ofdrw-conterver)
- 官方文档: https://gitee.com/ofdrw/ofdrw/blob/master/ofdrw-converter/doc/EXPORTER.md
4.1 介绍:
ofdrw-converter
的 OFDExporter
接口具有多个实现,其实现与导出的目的文档有关。
接口实现类命名格式为: 目标格式+Exporter
ofdrw-converter
支持导出为以下类型:
- 图片:
ImageExporter
- SVG矢量图形:
SVGExporter
- HTML网页:
HTMLExporter
- 纯文本:
TextExporter
- PDF文档:
PDFExporterIText
、PDFExporterPDFBox
4.2 Maven 依赖:
<dependency>
<groupId>org.ofdrw</groupId>
<artifactId>ofdrw-converter</artifactId>
<version>2.3.3</version>
</dependency>
4.3 导出为图片
导出OFD文档页面为图片,图片格式支持PNG、BPM、JPG。
实现类:org.ofdrw.converter.export.ImageExporter
注意事项:
- 可以通过构造器指定导出的图片类型,目前支持
PNG
、JPG
、BPM
,默认为PNG
格式。 - 可以通过构造器或方法设置导出图片的质量,也就是
ppm
参数,默认ppm
为15。 - 导出图片将存放于同一个目录,在该目录中图片以的页面索引作为文件名,如第1页的文件名为
0.png
。
示例:
Path ofdPath = Paths.get("src/test/resources/999.ofd");
Path imgDirPath = Paths.get("target/999.ofd/");
try (ImageExporter exporter = new ImageExporter(ofdPath, imgDirPath, "PNG", 20d)) {
exporter.export();
}
效果如下:
特有方法 | 用途 |
---|---|
List<Path> getImgFilePaths() | 获取导出页面对应图片文件路径,列表中次序与导出时的页码次序一致。 |
void setPPM(double ppm) | 设置导出图片质量,单位为:每毫米像素数量。 |
详见 测试用例
4.4 导出为SVG图形
导出OFD文档页面为SVG图形,文本中的所有文字都将转换为矢量路径。
实现类:org.ofdrw.converter.export.SVGExporter
注意事项:
- 可以通过构造器或方法设置导出SVG图形大小,也就是
ppm
参数,默认ppm
为15。 - 导出SVG图形文件将存放于同一个目录,在该目录中以页面索引作为文件名,如第1页的文件名为
0.svg
。
示例:
Path ofdPath = Paths.get("src/test/resources/999.ofd");
Path svgPath = Paths.get("target/999.ofd/");
try (SVGExporter exporter = new SVGExporter(ofdPath, svgPath, 15d)) {
exporter.export();
}
效果如下:(背景已经变成透明色了)
特有方法 | 用途 |
---|---|
List<Path> getSvgFilePaths() | 获取导出页面对应SVG文件路径,列表中次序与导出时的页码次序一致。 |
void setPPM(double ppm) | 设置导出SVG大小,单位为:每毫米像素数量。 |
详见 测试用例
4.5 导出为HTML网页
导出OFD文档页面为HTML网页,需要浏览器支持HTML5才可正常预览,由于是基于SVG方案导出的HTML,因此导出文件可能较大。
实现类:org.ofdrw.converter.export.HTMLExporter
注意事项:
- 若您需要调整HTML网页样式,可以通过继承
HTMLExporter
并覆盖header
、booter
、margin_bottom
属性,使用自定义的HTML样式。 - 导出的HTML网页需要浏览器支持HTML5才可正常预览。
- 若页面文字内容由文字图元构成且都由Unicode组成,那么导出网页可能可以通过鼠标选中与复制。
示例:
Path ofdPath = Paths.get("src/test/resources/n.ofd");
Path htmlPath = Paths.get("target/n.html");
try (HTMLExporter exporter = new HTMLExporter(ofdPath, htmlPath)) {
exporter.export();
}
效果如下:
详见 测试用例
4.6 导出为文本
导出OFD文档页面为文本文件,并非所有OFD页面都能导出文本,只有符合特定条件的OFD才可导出。
实现类:org.ofdrw.converter.export.TextExporter
注意事项:
- 部分OFD文档由于采用字形索引来定位文字、有个OFD整个页面均为路径数据图元而不是文字图元、有的OFD页面整个都为图片等诸多原因,无法保证一定能够导出文本。
- 由于文本布局等各种因素,导出文本顺序也难以与原文文本顺序一致。
示例:
Path ofdPath = Paths.get("src/test/resources/999.ofd");
Path txtPath = Paths.get("target/999.txt");
try (TextExporter exporter = new TextExporter(ofdPath, txtPath)) {
exporter.export();
}
效果如下:
详见 测试用例
4.7 [不推荐] 导出为PDF
警告:不推荐导出为PDF,OFD本身就是国产的板式文件,非特殊场景没有必要导出为PDF文件,该模块将进入LTS状态,不再持续更新!
导出OFD文档页面为PDF文件,该导出根据实现所使用的库不一致具有两种导出实现。
实现类:
org.ofdrw.converter.export.PDFExporterIText
org.ofdrw.converter.export.PDFExporterPDFBox
注意事项:
- 导出无法保证文档效果一致性,若您有建设性意见请提交PR。
基于PDFBox实现示例:
Path ofdPath = Paths.get("src/test/resources/999.ofd");
Path pdfPath = Paths.get("target/999.pdf");
try (OFDExporter exporter = new PDFExporterPDFBox(ofdPath, pdfPath)) {
exporter.export();
}
详见 基于PDFBox导出 测试用例
基于iText实现示例:
Path ofdPath = Paths.get("src/test/resources/999.ofd");
Path pdfPath = Paths.get("target/999.pdf");
try (OFDExporter exporter = new PDFExporterIText(ofdPath, pdfPath)) {
exporter.export();
}
详见 基于iText导出 测试用例
效果如下:
五、OFD签署
5.1 在线生成 sm2 证书
- 在线生成地址: https://www.gmcrt.cn/gmcrt/index.jsp
生成证书如下所示:
5.2 制作 esl 印章
Java 实现代码如下:
/**
* 生成ESL印章
*/
public static void buildEsl() throws IOException, CertificateEncodingException {
String imagePath = "D:\\signature.jpg";
String sealerCertPath = "D:\\keystore\\sm2.p12";
String imageBase64 = java.util.Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(new File(imagePath)));
Path userP12Path = Paths.get(sealerCertPath);
PrivateKey sealerPrvKey;
try {
byte[] bytes = FileUtils.readFileToByteArray(new File(sealerCertPath));
sealerPrvKey = NativePKCS12Tools.ReadPrvKey(java.util.Base64.getEncoder().encodeToString(bytes), "ACGkaka", "123456");
} catch (Exception e) {
throw new RuntimeException(e);
}
Certificate signCert = null ;
try {
signCert = PKCS12Tools.ReadUserCert(userP12Path, "ACGkaka", "123456");
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
byte[] encoded = signCert.getEncoded();
String s1 = java.util.Base64.getEncoder().encodeToString(encoded);
System.out.println("s1="+s1);
System.out.println("imageBase64:"+imageBase64);
long start = System.currentTimeMillis();
Calendar now = Calendar.getInstance();
now.add(Calendar.YEAR, 2);
Date then = now.getTime();
BuildSESealBase64 seal = new BuildSESealBase64().setEsID(UUID.randomUUID().toString().replace("-", ""))//印章ID
.setSealImageBase64(imageBase64)//印章图片base64
.setSealName("ACGkaka测试章")//印章名称
.setBuildSealerCertBase64(s1)//制章人公钥
.setHeight(40)//印章高度单位毫米
.setWidth(40)//印章宽度单位毫米
.setSesheader("chinamobile sign")//印章头部信息
.setUseSealerCertBase64(s1)//用章人公钥
.setValidStartDate(new Date())//印章有效期 传空,固定读用章人证书有效
.setValidEndDate(then);//印章有效期传空,固定读用章人证书有效
try {
PrivateKey finalSealerPrvKey = sealerPrvKey;
Map<String, Object> stringObjectMap = SESeal.buildBase64(seal, new CASignInterface() {//制章人私钥签名方法(需要调用CA密码机服务)
@Override
public byte[] sign(byte[] data) {
Signature sg = null;
try {
sg = Signature.getInstance("SM3WithSM2", new BouncyCastleProvider());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
try {
sg.initSign(finalSealerPrvKey);
} catch (InvalidKeyException e) {
e.printStackTrace();
}
try {
sg.update(data);
} catch (SignatureException e) {
e.printStackTrace();
}
byte[] sigVal = new byte[0];
try {
sigVal = sg.sign();
} catch (SignatureException e) {
e.printStackTrace();
}
System.out.println(sigVal.length);
return sigVal;
}
});
String s = stringObjectMap.get("base64").toString();
FileUtils.writeByteArrayToFile(new File("D:\\keystore\\sm2-signature.esl"), java.util.Base64.getDecoder().decode(s));
} catch (Exception e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("执行耗时间="+(end-start)/1000);
}
生成 ESL 印章如下所示:
5.3 坐标签署OFD
Java 实现代码如下所示:
/**
* OFD坐标签
*/
private static void signByXy() throws GeneralSecurityException, IOException {
Pos pos = new Pos();
pos.setPage(1);
pos.setX(100);
pos.setY(100);
pos.setWidth(40);
pos.setHeigh(40);
List<Pos> apList = new ArrayList<>();
apList.add(pos);
String req = "D:\\test.ofd";
String reqbase = java.util.Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(new File(req)));
String eslpath = "D:\\keystore\\sm2-signature.esl";
String eslbase = java.util.Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(new File(eslpath)));
Path userP12Path = Paths.get("D:\\keystore\\sm2.p12");
PrivateKey sealerPrvKey = null ;
try {
sealerPrvKey = PKCS12Tools.ReadPrvKey(userP12Path, "ACGkaka", "123456");
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
Certificate signCert = null ;
try {
signCert = PKCS12Tools.ReadUserCert(userP12Path, "ACGkaka", "123456");
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
byte[] encoded = signCert.getEncoded();
String s1 = java.util.Base64.getEncoder().encodeToString(encoded);
System.out.println("s1="+s1);
BuildOFDSignerBase64 buildOFDSigner = new BuildOFDSignerBase64()
.setOfdReqBase64(reqbase)
.setEslBase64(eslbase)
.setSignMode(0)
.setUseSealerCertBase64(s1)
.setApList(apList);
byte[] bytes = null;
try {
PrivateKey finalSealerPrvKey = sealerPrvKey;
bytes = OFDSigner.signByXyBase64(buildOFDSigner, new CASignInterface() {
@Override
public byte[] sign(byte[] data) {
Signature sg = null;
try {
sg = Signature.getInstance("SM3WithSM2", new BouncyCastleProvider());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
try {
sg.initSign(finalSealerPrvKey);
} catch (InvalidKeyException e) {
e.printStackTrace();
}
try {
sg.update(data);
} catch (SignatureException e) {
e.printStackTrace();
}
byte[] sigVal = new byte[0];
try {
sigVal = sg.sign();
} catch (SignatureException e) {
e.printStackTrace();
}
System.out.println(sigVal.length);
return sigVal;
}
});
}catch (Exception e){
e.printStackTrace();
}
FileUtils.writeByteArrayToFile(new File("D:\\test3.ofd"),bytes);
}
签署结果如下所示:
5.4 骑缝签署OFD
Java实现代码如下,除了 main() 方法之外还有 3 个方法:
public static void main(String[] args) throws IOException, GeneralSecurityException {
byte[] pdfBytes = FileUtils.readFileToByteArray(new File("D:\\test_2页.ofd"));
byte[] certBytes = FileUtils.readFileToByteArray(new File("D:\\keystore\\sm2.p12"));
String certBase64 = Base64.getEncoder().encodeToString(certBytes);
// 坐标签署
// List<ItemMapDTO> itemList = getItemList();
// byte[] newPdfBytes = OFDSignUtil.signSm2PDFBySeal(pdfBytes, itemList, "els", certBase64);
// FileUtils.writeByteArrayToFile(new File("D:\\test\\test2.ofd"), newPdfBytes);
// 骑缝签署
String side = "Right";
double offset = 40.0d;
byte[] eslBytes = FileUtils.readFileToByteArray(new File("D:\\keystore\\sm2-signature.esl"));
String eslBase64 = Base64.getEncoder().encodeToString(eslBytes);
String pdfBase64 = Base64.getEncoder().encodeToString(pdfBytes);
byte[] bytes1 = OFDSignUtil.signByRidingStampPos(pdfBase64, eslBase64, certBase64, side, offset);
FileUtils.writeByteArrayToFile(new File("D:\\test\\test3.ofd"), bytes1);
}
/**
* 骑缝签署
*
* @param reqBase64 待签署文件base64
* @param eslBase64 印章文件base64
* @param certBase64 用户证书base64
* @param side Left左骑缝 Right右骑缝(默认)
* @return 签署后文件
*/
public static byte[] signByRidingStampPos(String reqBase64, String eslBase64, String certBase64, String side, double offset) {
log.info("OFD骑缝签署开始");
BuildOFDSignerRideBase64 buildOFDSigner = new BuildOFDSignerRideBase64()
.setOfdReqBase64(reqBase64)
.setEslBase64(eslBase64)
.setSignMode(0)
.setUseSealerCertBase64(fileToInputStream(certBase64))
.setSide(side)
.setOffset(offset);
try {
byte[] bytes = OFDSigner.signByRidingStampPosBase64(buildOFDSigner, data -> {
try {
PrivateKey sealerPrvKey = NativePKCS12Tools.ReadPrvKey(certBase64, "ACGkaka", "123456");
Signature sg = null;
try {
sg = Signature.getInstance("SM3WithSM2", new BouncyCastleProvider());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
try {
sg.initSign(sealerPrvKey);
} catch (InvalidKeyException e) {
e.printStackTrace();
}
try {
sg.update(data);
} catch (SignatureException e) {
e.printStackTrace();
}
byte[] sigVal = new byte[0];
try {
sigVal = sg.sign();
} catch (SignatureException e) {
e.printStackTrace();
}
System.out.println(sigVal.length);
return sigVal;
} catch (Exception e) {
log.error("OFD骑缝签署异常", e);
throw new IllegalArgumentException("骑缝签署异常");
}
});
log.info("骑缝签署完成,签署后文件大小:{}kb", bytes == null ? 0 : bytes.length / 1024);
return bytes;
} catch (Exception e) {
log.error("OFD骑缝签署异常", e);
throw new IllegalArgumentException("骑缝签署异常");
}
}
/**
* 读取证书内容
* @param base64String
* @return
*/
public static String fileToInputStream(String base64String){
try {
Certificate signCert = PKCS12Tools.ReadUserCert(base64ToInputStream(base64String), "ACGkaka", "123456");
byte[] encoded = signCert.getEncoded();
return Base64.getEncoder().encodeToString(encoded);
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
签署结果如下所示:
5.5 验签 OFD
Java 实现代码如下:
/**
* 验签OFD
*/
public static void signVerify() throws IOException, GeneralSecurityException {
Path out = Paths.get("D:\\test3.ofd");
// 验证
try (OFDReader reader = new OFDReader(out);
OFDValidator validator = new OFDValidator(reader)) {
validator.setValidator(new SESV4ValidateContainer());
validator.exeValidate();
System.out.println(">> 验证通过");
}
}
验签结果如下:
整理完毕,完结撒花~ 🌻
参考地址:
1.科普 | ofd文件是什么,https://zhuanlan.zhihu.com/p/145599784
2.一文读懂 OFD 文件格式:国产 PDF,关键,重要,https://www.ithome.com/0/521/264.htm