SpringBoot实战(三十二)集成 ofdrw,实现 PDF 和 OFD 的转换、SM2 签署OFD

news2025/1/12 17:41:50

目录

    • 一、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

  • Gitee地址: https://gitee.com/ofdrw/ofdrw
  • GitHub地址: https://github.com/ofdrw/ofdrw

一、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 的文件格式呢?

主要出于以下几点原因:

  1. 格式不统一: 目前在国内可使用的版式文件格式包括 PDFCEB 等在内有不下十种,来自不同厂商和不同的技术,没有统一标准,就会导致不同机构、不同企业之间的文件交流存在阻碍,文件长期存档很困难。

  2. 难以自主可控: 我们知道,在目前办公文档市场上,来自 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-converterofdrw-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

注意事项:

  • 可以通过构造器指定导出的图片类型,目前支持PNGJPGBPM,默认为PNG格式。
  • 若图片格式不在上述范围您可能需要通过手动的方式设置加入图片大小。
  • 可以通过方法设置导出图片的质量,也就是ppm参数,默认ppm15(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-converterOFDExporter 接口具有多个实现,其实现与导出的目的文档有关。

接口实现类命名格式为: 目标格式+Exporter

ofdrw-converter 支持导出为以下类型:

  • 图片:ImageExporter
  • SVG矢量图形:SVGExporter
  • HTML网页:HTMLExporter
  • 纯文本:TextExporter
  • PDF文档:PDFExporterITextPDFExporterPDFBox

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

注意事项:

  • 可以通过构造器指定导出的图片类型,目前支持PNGJPGBPM,默认为PNG格式。
  • 可以通过构造器或方法设置导出图片的质量,也就是ppm参数,默认ppm15
  • 导出图片将存放于同一个目录,在该目录中图片以的页面索引作为文件名,如第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参数,默认ppm15
  • 导出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并覆盖headerbootermargin_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();
}

效果如下:

img.png

详见 测试用例

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2249827.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

QT6学习第四天 感受QT的文件编译

QT6学习第四天 感受QT的文件编译 使用纯代码编写程序新建工程 使用其他编辑器纯代码编写程序并在命令行运行使用 .ui 表单文件生成界面使用自定义 C 窗口类使用现成的QT Designer界面类 使用纯代码编写程序 我们知道QT Creator中可以用拖拽的方式在 .ui 文件上布局&#xff0c…

C++:用红黑树封装map与set-2

文章目录 前言一、红黑树封装map与set中const迭代器1. 框架的搭建2. set实现const迭代器3. map实现const迭代器 二、operator[ ]1. operator[ ]要达成的样子2. insert的改变 三. 解决insert里set中的问题四. 解决map中的operator[ ]总结用红黑树封装map与set代码 前言 前面我们…

微信小程序下拉刷新与上拉触底的全面教程

微信小程序下拉刷新与上拉触底的全面教程 引言 在微信小程序的开发中,用户体验至关重要。下拉刷新和上拉触底是提高用户交互体验的重要功能,能够让用户轻松获取最新数据和内容。本文将详细介绍这两个功能的实现方式,结合实际案例、代码示例和图片展示,帮助开发者轻松掌握…

【博主推荐】C#中winfrom开发常用技术点收集

文章目录 前言1.打开文件夹并选中文件2.窗体之间传参3.异步调用&#xff1a;让数据处理不影响页面操作4.创建一个多文档界面(MDI) 应用程序5.在WinForms中使用数据绑定6.在WinForms中后台使用控件的事件处理7.在WinForms中窗体跳转的几种方式8.后台处理方法中&#xff0c;调用窗…

第四十二篇 EfficientNet:重新思考卷积神经网络的模型缩放

文章目录 摘要1、简介2、相关工作3、复合模型缩放3.1、 问题公式化3.2、扩展维度3.3、复合比例 4、EfficientNet架构5、实验5.1、扩展MobileNets和ResNets5.2、EfficientNet的ImageNet结果5.3、EfficientNet的迁移学习结果 6、讨论7、结论 摘要 卷积神经网络(ConvNets)通常在固…

【Android】MMKV—高性能轻量化存储组件

【Android】MMKV—高性能轻量化存储组件 本文参考以及学习文档&#xff1a; Android存储&#xff1a;轻松掌握MMKV通过学习本文&#xff0c;轻松掌握腾讯开发的 MMKV 组件&#xff0c;尽早在项目中替换掉SharedPr - 掘金 MMKV——Android上的使用(替换SP存储)MMKV 是基于 mmap …

python+django自动化平台(一键执行sql) 前端vue-element展示

一、开发环境搭建和配置 pip install mysql-connector-pythonpip install PyMySQL二、django模块目录 dbOperations ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-313.pyc │ ├── admin.cpython-313.pyc │ ├── apps.cpython-313.pyc │ …

arm Rk1126 编译Qt工程报错: Could not find qmake spec

首先修改qmake.conf文件&#xff0c;配置好正确的交叉编译工具&#xff1a; 然后执行编译&#xff1a; /opt/Rv1126/Rv1126-盒子代码/rv1126-qt5-sdk/bin/qmake untitled.pro 报错。 原因&#xff1a;中文路径。修改路径为英文路径即可

[保姆式教程]使用labelimg2软件标注定向目标检测数据和格式转换

定向目标检测是一种在图像或视频中识别和定位对象的同时&#xff0c;还估计它们方向的技术。这种技术特别适用于处理有一定旋转或方向变化的对象&#xff0c;例如汽车、飞机或文本。定向目标检测器的输出是一组旋转的边界框&#xff0c;这些框精确地包围了图像中的对象&#xf…

C语言刷题笔记3(7)

7.1 数组处理斐波那契数列 题目描述:用数组来处理Fibonacci数列并输出。 输入:一个不超过40且大于2的整数n&#xff0c;表示需要处理并输出的Fibonacci数个数。 输出:输出前n个Fibonacci数&#xff0c;每行输出5个值&#xff0c;按每12位向右对齐的方式输出。请注意不要在第…

PHP 去掉特殊不可见字符 “\u200e“

描述 最近在排查网站业务时&#xff0c;发现有数据匹配失败的情况 肉眼上完全看不出问题所在 当把字符串 【M24308/23-14F‎】复制出来发现 末尾有个不可见的字符 使用删除键或左右移动时才会发现 最后测试通过 var_dump 打印 发现这个"空字符"占了三个长度 &#xf…

构建 LLM (大型语言模型)应用程序——从入门到精通(第七部分:开源 RAG)

通过检索增强生成 (RAG) 应用程序的视角学习大型语言模型 (LLM)。 本系列博文 简介数据准备句子转换器矢量数据库搜索与检索大语言模型开源 RAG&#xff08;本帖&#xff09;评估服务LLM高级 RAG 1. 简介 我们之前的博客文章广泛探讨了大型语言模型 (LLM)&#xff0c;涵盖了其…

linux基础2

声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&#…

智能产品综合开发 - 手势识别

1 实训选题目的 本次实训选择的题目是“基于树莓派的手势识别系统”&#xff0c;旨在为人们提供一种便捷的交互方式&#xff0c;使用户能够通过手势控制智能设备&#xff0c;摆脱传统的物理按键操作。通过本项目&#xff0c;我们希望能实现快速、灵活的手势识别&#xff0c;提升…

Qt常用控件之显示类控件

目录 QLabel 文本格式 设置图片 文本对齐/自动换行/边距/缩进 设置伙伴 QLCDNumber 倒计时功能 QProgressBar 进度条 QCalendarWidget QLabel QLabel 同样是 QWidget 的子类&#xff0c;所以前面博客中 QWidget 中的属性方法也是适用的 QLabel可以用来显示文本和图…

架构-微服务-环境搭建

文章目录 前言一、案例准备1. 技术选型2. 模块设计3. 微服务调用 二、创建父工程三、创建基础模块四、创建用户微服务五、创建商品微服务六、创建订单微服务 前言 ‌微服务环境搭建‌ 使用的电商项目中的商品、订单、用户为案例进行讲解。 一、案例准备 1. 技术选型 maven&a…

【JTAG】1149.6协议总结

【JTAG】1149.6协议详解-CSDN博客 IEEE 1149.6标准的基本实现需要在信号路径驱动器中添加一个时脉产生器&#xff0c;它能发射单一脉冲或一列脉冲&#xff0c;这取决于被加载到 1149.1 指令暂存器中的 EXTEST_PULSE 或 EXTEST_TRAIN 指令。1149.6在克服信道中共模讯号干扰能力…

小程序 - 个人简历

为了让招聘人员快速地认识自己&#xff0c;可以做一个“个人简历”微信小程序&#xff0c; 展示自己的个人信息。 下面将对“个人简历”微信小程序进行详细讲解。 目录 个人简历 创建图片目录 页面开发 index.wxml index.wxss 功能实现截图 总结 个人简历 创建图片目录…

Tülu 3:重新定义开源大模型的后训练范式

一、引言 在大型语言模型&#xff08;LLM&#xff09;的发展历程中&#xff0c;预训练阶段往往受到最多关注&#xff0c;动辄需要数百万美元算力投入和数万亿token的训练数据。然而&#xff0c;一个鲜为人知但同样关键的事实是&#xff1a;预训练完成的模型实际上并不能直接投…

systemverilog约束中:=和:/的区别

“x dist { [100:102] : 1, 200 : 2, 300 : 5}” 意味着其值等于100或101或102或200或300其中之一&#xff0c; 其权重比例为1:1:1:2:5 “x dist { [100:102] :/ 1, 200 : 2, 300 : 5}” 意味着等于100&#xff0c;101&#xff0c;102或200&#xff0c;或300其…